/*
 * Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.dv8tion.jda.internal.entities;

import gnu.trove.map.TLongObjectMap;
import gnu.trove.map.hash.TLongObjectHashMap;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.audit.ActionType;
import net.dv8tion.jda.api.audit.AuditLogChange;
import net.dv8tion.jda.api.audit.AuditLogEntry;
import net.dv8tion.jda.api.components.MessageTopLevelComponentUnion;
import net.dv8tion.jda.api.components.utils.ComponentDeserializer;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.entities.Guild.ExplicitContentLevel;
import net.dv8tion.jda.api.entities.Guild.NotificationLevel;
import net.dv8tion.jda.api.entities.Guild.Timeout;
import net.dv8tion.jda.api.entities.Guild.VerificationLevel;
import net.dv8tion.jda.api.entities.MessageEmbed.*;
import net.dv8tion.jda.api.entities.channel.Channel;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer;
import net.dv8tion.jda.api.entities.channel.attribute.IWebhookContainer;
import net.dv8tion.jda.api.entities.channel.concrete.*;
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel;
import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.entities.emoji.EmojiUnion;
import net.dv8tion.jda.api.entities.emoji.RichCustomEmoji;
import net.dv8tion.jda.api.entities.guild.SecurityIncidentActions;
import net.dv8tion.jda.api.entities.guild.SecurityIncidentDetections;
import net.dv8tion.jda.api.entities.messages.MessagePoll;
import net.dv8tion.jda.api.entities.messages.MessageSnapshot;
import net.dv8tion.jda.api.entities.sticker.*;
import net.dv8tion.jda.api.entities.templates.Template;
import net.dv8tion.jda.api.entities.templates.TemplateChannel;
import net.dv8tion.jda.api.entities.templates.TemplateGuild;
import net.dv8tion.jda.api.entities.templates.TemplateRole;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.update.*;
import net.dv8tion.jda.api.events.user.update.*;
import net.dv8tion.jda.api.exceptions.ParsingException;
import net.dv8tion.jda.api.interactions.DiscordLocale;
import net.dv8tion.jda.api.interactions.IntegrationOwners;
import net.dv8tion.jda.api.interactions.IntegrationType;
import net.dv8tion.jda.api.utils.cache.CacheFlag;
import net.dv8tion.jda.api.utils.cache.CacheView;
import net.dv8tion.jda.api.utils.data.DataArray;
import net.dv8tion.jda.api.utils.data.DataObject;
import net.dv8tion.jda.internal.JDAImpl;
import net.dv8tion.jda.internal.entities.channel.concrete.*;
import net.dv8tion.jda.internal.entities.channel.mixin.attribute.IPermissionContainerMixin;
import net.dv8tion.jda.internal.entities.emoji.ApplicationEmojiImpl;
import net.dv8tion.jda.internal.entities.emoji.CustomEmojiImpl;
import net.dv8tion.jda.internal.entities.emoji.RichCustomEmojiImpl;
import net.dv8tion.jda.internal.entities.emoji.UnicodeEmojiImpl;
import net.dv8tion.jda.internal.entities.messages.MessagePollImpl;
import net.dv8tion.jda.internal.entities.sticker.*;
import net.dv8tion.jda.internal.handle.EventCache;
import net.dv8tion.jda.internal.interactions.IntegrationOwnersImpl;
import net.dv8tion.jda.internal.utils.Helpers;
import net.dv8tion.jda.internal.utils.JDALogger;
import net.dv8tion.jda.internal.utils.UnlockHook;
import net.dv8tion.jda.internal.utils.cache.ChannelCacheViewImpl;
import net.dv8tion.jda.internal.utils.cache.MemberCacheViewImpl;
import net.dv8tion.jda.internal.utils.cache.SnowflakeCacheViewImpl;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.map.CaseInsensitiveMap;
import org.slf4j.Logger;

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.*;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class EntityBuilder extends AbstractEntityBuilder {
    public static final Logger LOG = JDALogger.getLog(EntityBuilder.class);
    public static final String MISSING_CHANNEL = "MISSING_CHANNEL";
    public static final String MISSING_USER = "MISSING_USER";
    public static final String UNKNOWN_MESSAGE_TYPE = "UNKNOWN_MESSAGE_TYPE";
    public static final ComponentDeserializer DEFAULT_COMPONENT_DESERIALIZER =
            new ComponentDeserializer(Collections.emptyList());
    private static final Set<String> richGameFields;

    static {
        Set<String> tmp = new HashSet<>();
        tmp.add("application_id");
        tmp.add("assets");
        tmp.add("details");
        tmp.add("flags");
        tmp.add("party");
        tmp.add("session_id");
        tmp.add("sync_id");
        richGameFields = Collections.unmodifiableSet(tmp);
    }

    public EntityBuilder(JDAImpl api) {
        super(api);
    }

    public SelfUser createSelfUser(DataObject self) {
        SelfUserImpl selfUser = (SelfUserImpl) (getJDA().hasSelfUser() ? getJDA().getSelfUser() : null);
        if (selfUser == null) {
            long id = self.getLong("id");
            selfUser = new SelfUserImpl(id, getJDA());
            getJDA().setSelfUser(selfUser);
        }

        SnowflakeCacheViewImpl<User> userView = getJDA().getUsersView();
        try (UnlockHook hook = userView.writeLock()) {
            if (userView.getElementById(selfUser.getIdLong()) == null) {
                userView.getMap().put(selfUser.getIdLong(), selfUser);
            }
        }

        if (!self.isNull("application_id")) {
            selfUser.setApplicationId(self.getUnsignedLong("application_id"));
        }
        selfUser.setVerified(self.getBoolean("verified"))
                .setMfaEnabled(self.getBoolean("mfa_enabled"))
                .setName(self.getString("username"))
                .setGlobalName(self.getString("global_name", null))
                .setDiscriminator(Short.parseShort(self.getString("discriminator", "0")))
                .setAvatarId(self.getString("avatar", null))
                .setBot(self.getBoolean("bot"))
                .setSystem(false);

        return selfUser;
    }

    public static Activity createActivity(String name, String url, Activity.ActivityType type) {
        return new ActivityImpl(name, url, type);
    }

    // Unlike Emoji.fromData this does not check for null or empty
    public static EmojiUnion createEmoji(DataObject emoji) {
        return createEmoji(emoji, "name", "id");
    }

    // Unlike Emoji.fromData this does not check for null or empty
    public static EmojiUnion createEmoji(DataObject emoji, String nameKey, String idKey) {
        long id = emoji.getUnsignedLong(idKey, 0L);
        if (id == 0L) {
            return new UnicodeEmojiImpl(emoji.getString(nameKey));
        } else { // name can be empty in some cases where discord fails to properly load the emoji
            return new CustomEmojiImpl(emoji.getString(nameKey, ""), id, emoji.getBoolean("animated"));
        }
    }

    private void createGuildEmojiPass(GuildImpl guildObj, DataArray array) {
        if (!getJDA().isCacheFlagSet(CacheFlag.EMOJI)) {
            return;
        }
        SnowflakeCacheViewImpl<RichCustomEmoji> emojiView = guildObj.getEmojisView();
        try (UnlockHook hook = emojiView.writeLock()) {
            TLongObjectMap<RichCustomEmoji> emojiMap = emojiView.getMap();
            for (int i = 0; i < array.length(); i++) {
                DataObject object = array.getObject(i);
                if (object.isNull("id")) {
                    LOG.error("Received GUILD_CREATE with an emoji with a null ID. JSON: {}", object);
                    continue;
                }
                long emojiId = object.getLong("id");
                emojiMap.put(emojiId, createEmoji(guildObj, object));
            }
        }
    }

    private void createScheduledEventPass(GuildImpl guildObj, DataArray array) {
        if (!getJDA().isCacheFlagSet(CacheFlag.SCHEDULED_EVENTS)) {
            return;
        }
        for (int i = 0; i < array.length(); i++) {
            DataObject object = array.getObject(i);
            try {
                if (object.isNull("id")) {
                    LOG.error("Received GUILD_CREATE with a scheduled event with a null ID. JSON: {}", object);
                    continue;
                }
                createScheduledEvent(guildObj, object);
            } catch (ParsingException exception) {
                LOG.error(
                        "Received GUILD_CREATE with a scheduled event that failed to parse. JSON: {}",
                        object,
                        exception);
            }
        }
    }

    private void createGuildStickerPass(GuildImpl guildObj, DataArray array) {
        if (!getJDA().isCacheFlagSet(CacheFlag.STICKER)) {
            return;
        }
        SnowflakeCacheViewImpl<GuildSticker> stickerView = guildObj.getStickersView();
        try (UnlockHook hook = stickerView.writeLock()) {
            TLongObjectMap<GuildSticker> stickerMap = stickerView.getMap();
            for (int i = 0; i < array.length(); i++) {
                DataObject object = array.getObject(i);
                if (object.isNull("id")) {
                    LOG.error(
                            "Received GUILD_CREATE with a sticker with a null ID. GuildId: {} JSON: {}",
                            guildObj.getId(),
                            object);
                    continue;
                }
                if (object.getInt("type", -1) != Sticker.Type.GUILD.getId()) {
                    LOG.error(
                            "Received GUILD_CREATE with sticker that had an unexpected type. GuildId: {} Type: {} JSON: {}",
                            guildObj.getId(),
                            object.getInt("type", -1),
                            object);
                    continue;
                }

                RichSticker sticker = createRichSticker(object);
                stickerMap.put(sticker.getIdLong(), (GuildSticker) sticker);
            }
        }
    }

    public SecurityIncidentActions createSecurityIncidentsActions(DataObject data) {
        OffsetDateTime invitesDisabledUntil = data.getOffsetDateTime("invites_disabled_until", null);
        OffsetDateTime dmsDisabledUntil = data.getOffsetDateTime("dms_disabled_until", null);

        if (invitesDisabledUntil == null && dmsDisabledUntil == null) {
            return null;
        }

        return SecurityIncidentActions.enabled(invitesDisabledUntil, dmsDisabledUntil);
    }

    public SecurityIncidentDetections createSecurityIncidentsDetections(DataObject data) {
        String timeDmSpamDetected = data.getString("dm_spam_detected_at", null);
        String timeRaidDetected = data.getString("raid_detected_at", null);

        if (timeRaidDetected == null && timeDmSpamDetected == null) {
            return null;
        }

        return new SecurityIncidentDetections(
                timeDmSpamDetected == null ? 0 : Helpers.toTimestamp(timeDmSpamDetected),
                timeRaidDetected == null ? 0 : Helpers.toTimestamp(timeRaidDetected));
    }

    public GuildImpl createGuild(
            long guildId, DataObject guildJson, TLongObjectMap<DataObject> members, int memberCount) {
        GuildImpl guildObj = new GuildImpl(getJDA(), guildId);
        String name = guildJson.getString("name", "");
        String iconId = guildJson.getString("icon", null);
        String splashId = guildJson.getString("splash", null);
        String description = guildJson.getString("description", null);
        String vanityCode = guildJson.getString("vanity_url_code", null);
        String bannerId = guildJson.getString("banner", null);
        String locale = guildJson.getString("preferred_locale", "en-US");
        SecurityIncidentActions securityIncidentActions = guildJson
                .optObject("incidents_data")
                .map(this::createSecurityIncidentsActions)
                .orElse(null);
        SecurityIncidentDetections securityIncidentDetections = guildJson
                .optObject("incidents_data")
                .map(this::createSecurityIncidentsDetections)
                .orElse(null);
        DataArray roleArray = guildJson.getArray("roles");
        DataArray channelArray = guildJson.getArray("channels");
        DataArray threadArray = guildJson.getArray("threads");
        DataArray scheduledEventsArray = guildJson.getArray("guild_scheduled_events");
        DataArray emojisArray = guildJson.getArray("emojis");
        DataArray voiceStateArray = guildJson.getArray("voice_states");
        Optional<DataArray> stickersArray = guildJson.optArray("stickers");
        Optional<DataArray> featuresArray = guildJson.optArray("features");
        Optional<DataArray> presencesArray = guildJson.optArray("presences");
        long ownerId = guildJson.getUnsignedLong("owner_id", 0L);
        long afkChannelId = guildJson.getUnsignedLong("afk_channel_id", 0L);
        long systemChannelId = guildJson.getUnsignedLong("system_channel_id", 0L);
        long rulesChannelId = guildJson.getUnsignedLong("rules_channel_id", 0L);
        long communityUpdatesChannelId = guildJson.getUnsignedLong("public_updates_channel_id", 0L);
        long safetyAlertsChannelId = guildJson.getUnsignedLong("safety_alerts_channel_id", 0L);
        int boostCount = guildJson.getInt("premium_subscription_count", 0);
        int boostTier = guildJson.getInt("premium_tier", 0);
        int maxMembers = guildJson.getInt("max_members", 0);
        int maxPresences = guildJson.getInt("max_presences", 5000);
        int mfaLevel = guildJson.getInt("mfa_level", 0);
        int afkTimeout = guildJson.getInt("afk_timeout", 0);
        int verificationLevel = guildJson.getInt("verification_level", 0);
        int notificationLevel = guildJson.getInt("default_message_notifications", 0);
        int explicitContentLevel = guildJson.getInt("explicit_content_filter", 0);
        int nsfwLevel = guildJson.getInt("nsfw_level", -1);
        boolean boostProgressBarEnabled = guildJson.getBoolean("premium_progress_bar_enabled");
        int systemChannelFlags = guildJson.getInt("system_channel_flags", 0);

        guildObj.setName(name)
                .setIconId(iconId)
                .setSplashId(splashId)
                .setDescription(description)
                .setBannerId(bannerId)
                .setVanityCode(vanityCode)
                .setMaxMembers(maxMembers)
                .setMaxPresences(maxPresences)
                .setOwnerId(ownerId)
                .setAfkTimeout(Guild.Timeout.fromKey(afkTimeout))
                .setSecurityIncidentActions(securityIncidentActions)
                .setSecurityIncidentDetections(securityIncidentDetections)
                .setVerificationLevel(VerificationLevel.fromKey(verificationLevel))
                .setDefaultNotificationLevel(Guild.NotificationLevel.fromKey(notificationLevel))
                .setExplicitContentLevel(Guild.ExplicitContentLevel.fromKey(explicitContentLevel))
                .setRequiredMFALevel(Guild.MFALevel.fromKey(mfaLevel))
                .setLocale(DiscordLocale.from(locale))
                .setBoostCount(boostCount)
                .setBoostTier(boostTier)
                .setMemberCount(memberCount)
                .setNSFWLevel(Guild.NSFWLevel.fromKey(nsfwLevel))
                .setBoostProgressBarEnabled(boostProgressBarEnabled)
                .setSystemChannelFlags(systemChannelFlags);

        SnowflakeCacheViewImpl<Guild> guildView = getJDA().getGuildsView();
        try (UnlockHook hook = guildView.writeLock()) {
            guildView.getMap().put(guildId, guildObj);
        }

        Set<String> features = featuresArray
                .map(array -> array.stream(DataArray::getString)
                        .map(String::intern) // Prevent allocating the same feature string over and over
                        .collect(Collectors.toSet()))
                .orElse(Collections.emptySet());
        guildObj.setFeatures(features);

        SnowflakeCacheViewImpl<Role> roleView = guildObj.getRolesView();
        try (UnlockHook hook = roleView.writeLock()) {
            TLongObjectMap<Role> map = roleView.getMap();
            for (int i = 0; i < roleArray.length(); i++) {
                DataObject obj = roleArray.getObject(i);
                Role role = createRole(guildObj, obj, guildId);
                map.put(role.getIdLong(), role);
                if (role.getIdLong() == guildObj.getIdLong()) {
                    guildObj.setPublicRole(role);
                }
            }
        }

        for (int i = 0; i < channelArray.length(); i++) {
            DataObject channelJson = channelArray.getObject(i);
            createGuildChannel(guildObj, channelJson);
        }

        TLongObjectMap<DataObject> voiceStates =
                Helpers.convertToMap((o) -> o.getUnsignedLong("user_id", 0L), voiceStateArray);
        TLongObjectMap<DataObject> presences = presencesArray
                .map(o1 -> Helpers.convertToMap(o2 -> o2.getObject("user").getUnsignedLong("id"), o1))
                .orElseGet(TLongObjectHashMap::new);
        try (UnlockHook h1 = guildObj.getMembersView().writeLock();
                UnlockHook h2 = getJDA().getUsersView().writeLock()) {
            // Add members to cache when subscriptions are disabled when they appear here
            // this is done because we can still keep track of members in voice channels
            for (DataObject memberJson : members.valueCollection()) {
                long userId = memberJson.getObject("user").getUnsignedLong("id");
                DataObject voiceState = voiceStates.get(userId);
                DataObject presence = presences.get(userId);
                updateMemberCache(createMember(guildObj, memberJson, voiceState, presence));
            }
        }

        if (guildObj.getOwner() == null) {
            LOG.debug(
                    "Finished setup for guild with a null owner. GuildId: {} OwnerId: {}",
                    guildId,
                    guildJson.opt("owner_id").orElse(null));
        }
        if (guildObj.getMember(api.getSelfUser()) == null) {
            LOG.error("Guild is missing a SelfMember. GuildId: {}", guildId);
            LOG.debug("Guild is missing a SelfMember. GuildId: {} JSON: \n{}", guildId, guildJson);
            // This is actually a gateway request
            guildObj.retrieveMembersByIds(api.getSelfUser().getIdLong()).onSuccess(m -> {
                if (m.isEmpty()) {
                    LOG.warn(
                            "Was unable to recover SelfMember for guild with id {}. This guild might be corrupted!",
                            guildId);
                } else {
                    LOG.debug("Successfully recovered SelfMember for guild with id {}.", guildId);
                }
            });
        }

        for (int i = 0; i < threadArray.length(); i++) {
            DataObject threadJson = threadArray.getObject(i);
            try {
                createThreadChannel(guildObj, threadJson, guildObj.getIdLong());
            } catch (Exception ex) {
                if (MISSING_CHANNEL.equals(ex.getMessage())) {
                    LOG.debug("Discarding thread without cached parent channel. JSON: {}", threadJson);
                } else {
                    LOG.warn(
                            "Failed to create thread channel for guild with id {}.\nJSON: {}", guildId, threadJson, ex);
                }
            }
        }

        createScheduledEventPass(guildObj, scheduledEventsArray);
        createGuildEmojiPass(guildObj, emojisArray);
        stickersArray.ifPresent(stickers -> createGuildStickerPass(guildObj, stickers));
        guildJson
                .optArray("stage_instances")
                .map(arr -> arr.stream(DataArray::getObject))
                .ifPresent(list -> list.forEach(it -> createStageInstance(guildObj, it)));

        guildObj.setAfkChannel(guildObj.getVoiceChannelById(afkChannelId))
                .setSystemChannel(guildObj.getTextChannelById(systemChannelId))
                .setRulesChannel(guildObj.getTextChannelById(rulesChannelId))
                .setCommunityUpdatesChannel(guildObj.getTextChannelById(communityUpdatesChannelId))
                .setSafetyAlertsChannel(guildObj.getTextChannelById(safetyAlertsChannelId));

        return guildObj;
    }

    public GuildChannel createGuildChannel(GuildImpl guildObj, DataObject channelData) {
        ChannelType channelType = ChannelType.fromId(channelData.getInt("type"));
        switch (channelType) {
            case TEXT:
                return createTextChannel(guildObj, channelData, guildObj.getIdLong());
            case NEWS:
                return createNewsChannel(guildObj, channelData, guildObj.getIdLong());
            case STAGE:
                return createStageChannel(guildObj, channelData, guildObj.getIdLong());
            case VOICE:
                return createVoiceChannel(guildObj, channelData, guildObj.getIdLong());
            case CATEGORY:
                return createCategory(guildObj, channelData, guildObj.getIdLong());
            case FORUM:
                return createForumChannel(guildObj, channelData, guildObj.getIdLong());
            case MEDIA:
                return createMediaChannel(guildObj, channelData, guildObj.getIdLong());
            default:
                LOG.debug("Cannot create channel for type " + channelData.getInt("type"));
                return null;
        }
    }

    public User.PrimaryGuild createPrimaryGuild(DataObject obj) {
        if (obj.isNull("identity_guild_id")) {
            return null;
        }

        return new User.PrimaryGuild(
                obj.getUnsignedLong("identity_guild_id"),
                obj.getBoolean("identity_enabled"),
                obj.getString("tag", null),
                obj.getString("badge", null));
    }

    public UserImpl createUser(DataObject user) {
        boolean newUser = false;
        long id = user.getLong("id");
        UserImpl userObj;

        SnowflakeCacheViewImpl<User> userView = getJDA().getUsersView();
        try (UnlockHook hook = userView.readLock()) {
            userObj = (UserImpl) userView.getElementById(id);
            if (userObj == null) {
                userObj = new UserImpl(id, getJDA());
                newUser = true;
            }
        }

        User.Profile profile = user.hasKey("banner")
                ? new User.Profile(
                        id, user.getString("banner", null), user.getInt("accent_color", User.DEFAULT_ACCENT_COLOR_RAW))
                : null;

        if (newUser) {
            User.PrimaryGuild primaryGuild = user.optObject("primary_guild")
                    .map(this::createPrimaryGuild)
                    .orElse(null);

            // Initial creation
            userObj.setName(user.getString("username"))
                    .setGlobalName(user.getString("global_name", null))
                    .setDiscriminator(Short.parseShort(user.getString("discriminator", "0")))
                    .setAvatarId(user.getString("avatar", null))
                    .setBot(user.getBoolean("bot"))
                    .setSystem(user.getBoolean("system"))
                    .setFlags(user.getInt("public_flags", 0))
                    .setPrimaryGuild(primaryGuild)
                    .setProfile(profile);
        } else {
            // Fire update events
            updateUser(userObj, user);
        }

        return userObj;
    }

    public void updateUser(UserImpl userObj, DataObject user) {
        String oldName = userObj.getName();
        String newName = user.getString("username");
        String oldGlobalName = userObj.getGlobalName();
        String newGlobalName = user.getString("global_name", null);
        short oldDiscriminator = userObj.getDiscriminatorInt();
        short newDiscriminator = Short.parseShort(user.getString("discriminator", "0"));
        String oldAvatar = userObj.getAvatarId();
        String newAvatar = user.getString("avatar", null);
        int oldFlags = userObj.getFlagsRaw();
        int newFlags = user.getInt("public_flags", 0);
        User.PrimaryGuild oldPrimaryGuild = userObj.getPrimaryGuild();
        User.PrimaryGuild newPrimaryGuild =
                user.optObject("primary_guild").map(this::createPrimaryGuild).orElse(null);

        JDAImpl jda = getJDA();
        long responseNumber = jda.getResponseTotal();
        if (!oldName.equals(newName)) {
            userObj.setName(newName);
            jda.handleEvent(new UserUpdateNameEvent(
                    jda, responseNumber,
                    userObj, oldName));
        }

        if (!Objects.equals(oldGlobalName, newGlobalName)) {
            userObj.setGlobalName(newGlobalName);
            jda.handleEvent(new UserUpdateGlobalNameEvent(
                    jda, responseNumber,
                    userObj, oldGlobalName));
        }

        if (oldDiscriminator != newDiscriminator) {
            String oldDiscrimString = userObj.getDiscriminator();
            userObj.setDiscriminator(newDiscriminator);
            jda.handleEvent(new UserUpdateDiscriminatorEvent(
                    jda, responseNumber,
                    userObj, oldDiscrimString));
        }

        if (!Objects.equals(oldAvatar, newAvatar)) {
            userObj.setAvatarId(newAvatar);
            jda.handleEvent(new UserUpdateAvatarEvent(
                    jda, responseNumber,
                    userObj, oldAvatar));
        }

        if (oldFlags != newFlags) {
            userObj.setFlags(newFlags);
            jda.handleEvent(new UserUpdateFlagsEvent(jda, responseNumber, userObj, User.UserFlag.getFlags(oldFlags)));
        }

        if (!Objects.equals(oldPrimaryGuild, newPrimaryGuild)) {
            userObj.setPrimaryGuild(newPrimaryGuild);
            jda.handleEvent(new UserUpdatePrimaryGuildEvent(
                    jda, responseNumber,
                    userObj, oldPrimaryGuild));
        }
    }

    public boolean updateMemberCache(MemberImpl member) {
        return updateMemberCache(member, false);
    }

    public boolean updateMemberCache(MemberImpl member, boolean forceRemove) {
        GuildImpl guild = member.getGuild();
        UserImpl user = (UserImpl) member.getUser();
        MemberCacheViewImpl membersView = guild.getMembersView();
        if (forceRemove || !getJDA().cacheMember(member)) {
            if (membersView.remove(member.getIdLong()) == null) {
                return false;
            }
            LOG.trace("Unloading member {}", member);
            if (user.getMutualGuilds().isEmpty()) {
                // we no longer share any guilds/channels with this user so remove it from cache
                getJDA().getUsersView().remove(user.getIdLong());
            }
            return false;
        } else if (guild.getMemberById(member.getIdLong()) != null) {
            // Member should be added to cache but already is cached -> do nothing
            return true;
        }

        LOG.trace("Loading member {}", member);

        if (getJDA().getUserById(user.getIdLong()) == null) {
            SnowflakeCacheViewImpl<User> usersView = getJDA().getUsersView();
            try (UnlockHook hook1 = usersView.writeLock()) {
                usersView.getMap().put(user.getIdLong(), user);
            }
        }

        try (UnlockHook hook = membersView.writeLock()) {
            membersView.getMap().put(member.getIdLong(), member);
            if (member.isOwner()) {
                guild.setOwner(member);
            }
        }

        long hashId = guild.getIdLong() ^ user.getIdLong();
        getJDA().getEventCache().playbackCache(EventCache.Type.USER, member.getIdLong());
        getJDA().getEventCache().playbackCache(EventCache.Type.MEMBER, hashId);
        return true;
    }

    public MemberImpl createMember(GuildImpl guild, DataObject memberJson) {
        return createMember(guild, memberJson, null, null);
    }

    public MemberImpl createMember(
            GuildImpl guild, DataObject memberJson, DataObject voiceStateJson, DataObject presence) {
        User user = createUser(memberJson.getObject("user"));
        DataArray roleArray = memberJson.getArray("roles");
        MemberImpl member = (MemberImpl) guild.getMember(user);
        if (member == null) {
            // Create a brand new member
            if (user.getIdLong() == getJDA().getSelfUser().getIdLong()) {
                member = new SelfMemberImpl(guild, ((SelfUser) user));
            } else {
                member = new MemberImpl(guild, user);
            }
            configureMember(memberJson, member);
            Set<Role> roles = member.getRoleSet();
            for (int i = 0; i < roleArray.length(); i++) {
                long roleId = roleArray.getUnsignedLong(i);
                Role role = guild.getRoleById(roleId);
                if (role != null) {
                    roles.add(role);
                }
            }
        } else {
            // Update cached member and fire events
            List<Role> roles = new ArrayList<>(roleArray.length());
            for (int i = 0; i < roleArray.length(); i++) {
                long roleId = roleArray.getUnsignedLong(i);
                Role role = guild.getRoleById(roleId);
                if (role != null) {
                    roles.add(role);
                }
            }
            updateMember(guild, member, memberJson, roles);
        }

        // Load voice state and presence if necessary
        if (voiceStateJson != null) {
            createGuildVoiceState(member, voiceStateJson);
        }
        if (presence != null) {
            createPresence(member, presence);
        }

        // Make sure the voice states always have the latest member reference,
        // even when member is uncached
        guild.updateCacheVoiceStateMember(member);
        return member;
    }

    public GuildVoiceState createGuildVoiceState(MemberImpl member, DataObject voiceStateJson) {
        GuildVoiceStateImpl voiceState = member.getVoiceState();
        if (voiceState == null) {
            voiceState = new GuildVoiceStateImpl(member);
        }
        updateGuildVoiceState(voiceState, voiceStateJson, member);
        return voiceState;
    }

    private void updateGuildVoiceState(
            GuildVoiceStateImpl currentVoiceState, DataObject newVoiceStateJson, MemberImpl member) {
        GuildImpl guild = member.getGuild();

        long channelId = newVoiceStateJson.getLong("channel_id");
        AudioChannel audioChannel = guild.getChannelById(AudioChannel.class, channelId);
        if (audioChannel == null) {
            LOG.warn(
                    "Received a GuildVoiceState with a channel ID for a non-existent channel! ChannelId: {} GuildId: {} UserId: {}",
                    channelId,
                    guild.getId(),
                    member.getId());
        }

        String requestToSpeak = newVoiceStateJson.getString("request_to_speak_timestamp", null);
        OffsetDateTime timestamp = null;
        if (requestToSpeak != null) {
            timestamp = OffsetDateTime.parse(requestToSpeak);
        }

        // VoiceState is considered volatile so we don't expect anything to actually exist
        currentVoiceState
                .setSelfMuted(newVoiceStateJson.getBoolean("self_mute"))
                .setSelfDeafened(newVoiceStateJson.getBoolean("self_deaf"))
                .setGuildMuted(newVoiceStateJson.getBoolean("mute"))
                .setGuildDeafened(newVoiceStateJson.getBoolean("deaf"))
                .setSuppressed(newVoiceStateJson.getBoolean("suppress"))
                .setSessionId(newVoiceStateJson.getString("session_id"))
                .setStream(newVoiceStateJson.getBoolean("self_stream"))
                .setRequestToSpeak(timestamp)
                .updateConnectedChannel(audioChannel);
    }

    public void updateMember(GuildImpl guild, MemberImpl member, DataObject content, List<Role> newRoles) {
        // If newRoles is null that means that we didn't find a role
        // that was in the array and was cached this event
        long responseNumber = getJDA().getResponseTotal();
        if (newRoles != null) {
            updateMemberRoles(member, newRoles, responseNumber);
        }

        if (content.hasKey("nick")) {
            String oldNick = member.getNickname();
            String newNick = content.getString("nick", null);
            if (!Objects.equals(oldNick, newNick)) {
                member.setNickname(newNick);
                getJDA().handleEvent(new GuildMemberUpdateNicknameEvent(getJDA(), responseNumber, member, oldNick));
            }
        }
        if (content.hasKey("avatar")) {
            String oldAvatarId = member.getAvatarId();
            String newAvatarId = content.getString("avatar", null);
            if (!Objects.equals(oldAvatarId, newAvatarId)) {
                member.setAvatarId(newAvatarId);
                getJDA().handleEvent(new GuildMemberUpdateAvatarEvent(getJDA(), responseNumber, member, oldAvatarId));
            }
        }
        if (content.hasKey("premium_since")) {
            long epoch = 0;
            if (!content.isNull("premium_since")) {
                epoch = Helpers.toTimestamp(content.getString("premium_since"));
            }
            if (epoch != member.getBoostDateRaw()) {
                OffsetDateTime oldTime = member.getTimeBoosted();
                member.setBoostDate(epoch);
                getJDA().handleEvent(new GuildMemberUpdateBoostTimeEvent(getJDA(), responseNumber, member, oldTime));
            }
        }

        if (content.hasKey("communication_disabled_until")) {
            long epoch = 0;
            if (!content.isNull("communication_disabled_until")) {
                epoch = Helpers.toTimestamp(content.getString("communication_disabled_until"));
            }
            if (epoch != member.getTimeOutEndRaw()) {
                OffsetDateTime oldTime = member.getTimeOutEnd();
                member.setTimeOutEnd(epoch);
                getJDA().handleEvent(new GuildMemberUpdateTimeOutEvent(getJDA(), responseNumber, member, oldTime));
            }
        }

        if (!content.isNull("joined_at") && !member.hasTimeJoined()) {
            String joinedAtRaw = content.getString("joined_at");
            TemporalAccessor joinedAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(joinedAtRaw);
            long joinEpoch = Instant.from(joinedAt).toEpochMilli();
            member.setJoinDate(joinEpoch);
        }

        if (!content.isNull("pending")) {
            boolean pending = content.getBoolean("pending");
            boolean oldPending = member.isPending();
            if (pending != oldPending) {
                member.setPending(pending);
                getJDA().handleEvent(new GuildMemberUpdatePendingEvent(getJDA(), responseNumber, member, oldPending));
            }
        }

        if (!content.isNull("flags")) {
            int flags = content.getInt("flags");
            int oldFlags = member.getFlagsRaw();
            if (flags != oldFlags) {
                member.setFlags(flags);
                getJDA().handleEvent(new GuildMemberUpdateFlagsEvent(
                        getJDA(), responseNumber, member, Member.MemberFlag.fromRaw(oldFlags)));
            }
        }

        updateUser((UserImpl) member.getUser(), content.getObject("user"));
    }

    private void updateMemberRoles(MemberImpl member, List<Role> newRoles, long responseNumber) {
        Set<Role> currentRoles = member.getRoleSet();
        // Find the roles removed.
        List<Role> removedRoles = new LinkedList<>();
        each:
        for (Role role : currentRoles) {
            for (Iterator<Role> it = newRoles.iterator(); it.hasNext(); ) {
                Role r = it.next();
                if (role.equals(r)) {
                    it.remove();
                    continue each;
                }
            }
            removedRoles.add(role);
        }

        if (removedRoles.size() > 0) {
            currentRoles.removeAll(removedRoles);
        }
        if (newRoles.size() > 0) {
            currentRoles.addAll(newRoles);
        }

        if (removedRoles.size() > 0) {
            getJDA().handleEvent(new GuildMemberRoleRemoveEvent(getJDA(), responseNumber, member, removedRoles));
        }
        if (newRoles.size() > 0) {
            getJDA().handleEvent(new GuildMemberRoleAddEvent(getJDA(), responseNumber, member, newRoles));
        }
    }

    public void createPresence(MemberImpl member, DataObject presenceJson) {
        if (member == null) {
            throw new NullPointerException("Provided member was null!");
        }
        OnlineStatus onlineStatus = OnlineStatus.fromKey(presenceJson.getString("status"));
        if (onlineStatus == OnlineStatus.OFFLINE) {
            return; // don't cache offline member presences!
        }
        MemberPresenceImpl presence = member.getPresence();
        if (presence == null) {
            CacheView.SimpleCacheView<MemberPresenceImpl> view =
                    member.getGuild().getPresenceView();
            if (view == null) {
                return;
            }
            presence = new MemberPresenceImpl();
            try (UnlockHook lock = view.writeLock()) {
                view.getMap().put(member.getIdLong(), presence);
            }
        }

        boolean cacheGame = getJDA().isCacheFlagSet(CacheFlag.ACTIVITY);
        boolean cacheStatus = getJDA().isCacheFlagSet(CacheFlag.CLIENT_STATUS);

        DataArray activityArray =
                !cacheGame || presenceJson.isNull("activities") ? null : presenceJson.getArray("activities");
        DataObject clientStatusJson =
                !cacheStatus || presenceJson.isNull("client_status") ? null : presenceJson.getObject("client_status");
        List<Activity> activities = new ArrayList<>();
        boolean parsedActivity = false;

        if (cacheGame && activityArray != null) {
            for (int i = 0; i < activityArray.length(); i++) {
                try {
                    activities.add(createActivity(activityArray.getObject(i)));
                    parsedActivity = true;
                } catch (Exception ex) {
                    String userId = member.getId();
                    if (LOG.isDebugEnabled()) {
                        LOG.warn(
                                "Encountered exception trying to parse a presence! UserId: {} JSON: {}",
                                userId,
                                activityArray,
                                ex);
                    } else {
                        LOG.warn(
                                "Encountered exception trying to parse a presence! UserId: {} Message: {} Enable debug for details",
                                userId,
                                ex.getMessage());
                    }
                }
            }
        }
        if (cacheGame && parsedActivity) {
            presence.setActivities(activities);
        }
        presence.setOnlineStatus(onlineStatus);
        if (clientStatusJson != null) {
            for (String key : clientStatusJson.keys()) {
                ClientType type = ClientType.fromKey(key);
                OnlineStatus status = OnlineStatus.fromKey(clientStatusJson.getString(key));
                presence.setOnlineStatus(type, status);
            }
        }
    }

    public static Activity createActivity(DataObject gameJson) {
        String name = String.valueOf(gameJson.get("name"));
        String url = gameJson.isNull("url") ? null : String.valueOf(gameJson.get("url"));
        Activity.ActivityType type;
        try {
            type = gameJson.isNull("type")
                    ? Activity.ActivityType.PLAYING
                    : Activity.ActivityType.fromKey(
                            Integer.parseInt(gameJson.get("type").toString()));
        } catch (NumberFormatException e) {
            type = Activity.ActivityType.PLAYING;
        }

        Activity.Timestamps timestamps = null;
        if (!gameJson.isNull("timestamps")) {
            DataObject obj = gameJson.getObject("timestamps");
            long start, end;
            start = obj.getLong("start", 0L);
            end = obj.getLong("end", 0L);
            timestamps = new Activity.Timestamps(start, end);
        }

        EmojiUnion emoji = null;
        if (!gameJson.isNull("emoji")) {
            emoji = createEmoji(gameJson.getObject("emoji"));
        }

        if (type == Activity.ActivityType.CUSTOM_STATUS) {
            if (gameJson.hasKey("state")) {
                name = gameJson.getString("state", "");
                gameJson = gameJson.remove("state");
            }
        }

        String state = gameJson.isNull("state") ? null : String.valueOf(gameJson.get("state"));

        if (!CollectionUtils.containsAny(gameJson.keys(), richGameFields)) {
            return new ActivityImpl(name, state, url, type, timestamps, emoji);
        }

        // data for spotify
        long id = gameJson.getLong("application_id", 0L);
        String sessionId = gameJson.getString("session_id", null);
        String syncId = gameJson.getString("sync_id", null);
        int flags = gameJson.getInt("flags", 0);
        String details = gameJson.isNull("details") ? null : String.valueOf(gameJson.get("details"));

        RichPresence.Party party = null;
        if (!gameJson.isNull("party")) {
            DataObject obj = gameJson.getObject("party");
            String partyId = obj.isNull("id") ? null : obj.getString("id");
            DataArray sizeArr = obj.isNull("size") ? null : obj.getArray("size");
            long size = 0, max = 0;
            if (sizeArr != null && sizeArr.length() > 0) {
                size = sizeArr.getLong(0);
                max = sizeArr.length() < 2 ? 0 : sizeArr.getLong(1);
            }
            party = new RichPresence.Party(partyId, size, max);
        }

        String smallImageKey = null, smallImageText = null;
        String largeImageKey = null, largeImageText = null;
        if (!gameJson.isNull("assets")) {
            DataObject assets = gameJson.getObject("assets");
            if (!assets.isNull("small_image")) {
                smallImageKey = String.valueOf(assets.get("small_image"));
                smallImageText = assets.isNull("small_text") ? null : String.valueOf(assets.get("small_text"));
            }
            if (!assets.isNull("large_image")) {
                largeImageKey = String.valueOf(assets.get("large_image"));
                largeImageText = assets.isNull("large_text") ? null : String.valueOf(assets.get("large_text"));
            }
        }

        return new RichPresenceImpl(
                type,
                name,
                url,
                id,
                emoji,
                party,
                details,
                state,
                timestamps,
                syncId,
                sessionId,
                flags,
                largeImageKey,
                largeImageText,
                smallImageKey,
                smallImageText);
    }

    public RichCustomEmojiImpl createEmoji(GuildImpl guildObj, DataObject json) {
        DataArray emojiRoles = json.optArray("roles").orElseGet(DataArray::empty);
        long emojiId = json.getLong("id");
        User user = json.isNull("user") ? null : createUser(json.getObject("user"));
        RichCustomEmojiImpl emojiObj = (RichCustomEmojiImpl) guildObj.getEmojiById(emojiId);
        if (emojiObj == null) {
            emojiObj = new RichCustomEmojiImpl(emojiId, guildObj);
        }
        Set<Role> roleSet = emojiObj.getRoleSet();

        roleSet.clear();
        for (int j = 0; j < emojiRoles.length(); j++) {
            Role role = guildObj.getRoleById(emojiRoles.getString(j));
            if (role != null) {
                roleSet.add(role);
            }
        }
        if (user != null) {
            emojiObj.setOwner(user);
        }
        return emojiObj.setName(json.getString("name", ""))
                .setAnimated(json.getBoolean("animated"))
                .setManaged(json.getBoolean("managed"))
                .setAvailable(json.getBoolean("available", true));
    }

    public ApplicationEmojiImpl createApplicationEmoji(JDAImpl api, DataObject json, User owner) {
        long emojiId = json.getUnsignedLong("id");

        return new ApplicationEmojiImpl(emojiId, api, owner)
                .setAnimated(json.getBoolean("animated"))
                .setName(json.getString("name"));
    }

    public ScheduledEvent createScheduledEvent(GuildImpl guild, DataObject json) {
        long id = json.getLong("id");
        ScheduledEventImpl scheduledEvent =
                (ScheduledEventImpl) guild.getScheduledEventsView().get(id);
        if (scheduledEvent == null) {
            SnowflakeCacheViewImpl<ScheduledEvent> scheduledEventView = guild.getScheduledEventsView();
            try (UnlockHook hook = scheduledEventView.writeLock()) {
                scheduledEvent = new ScheduledEventImpl(id, guild);
                if (getJDA().isCacheFlagSet(CacheFlag.SCHEDULED_EVENTS)) {
                    scheduledEventView.getMap().put(id, scheduledEvent);
                }
            }
        }

        scheduledEvent
                .setName(json.getString("name"))
                .setDescription(json.getString("description", null))
                .setStatus(ScheduledEvent.Status.fromKey(json.getInt("status", -1)))
                .setInterestedUserCount(json.getInt("user_count", -1))
                .setStartTime(json.getOffsetDateTime("scheduled_start_time"))
                .setEndTime(json.getOffsetDateTime("scheduled_end_time", null))
                .setImage(json.getString("image", null));

        long creatorId = json.getLong("creator_id", 0);
        scheduledEvent.setCreatorId(creatorId);
        if (creatorId != 0) {
            if (json.hasKey("creator")) {
                scheduledEvent.setCreator(createUser(json.getObject("creator")));
            } else {
                scheduledEvent.setCreator(getJDA().getUserById(creatorId));
            }
        }
        ScheduledEvent.Type type = ScheduledEvent.Type.fromKey(json.getInt("entity_type"));
        scheduledEvent.setType(type);
        switch (type) {
            case STAGE_INSTANCE:
            case VOICE:
                scheduledEvent.setLocation(json.getString("channel_id"));
                break;
            case EXTERNAL:
                String externalLocation;
                if (json.isNull("entity_metadata")
                        || json.getObject("entity_metadata").isNull("location")) {
                    externalLocation = "";
                } else {
                    externalLocation = json.getObject("entity_metadata").getString("location");
                }

                scheduledEvent.setLocation(externalLocation);
        }
        return scheduledEvent;
    }

    public Category createCategory(DataObject json, long guildId) {
        return createCategory(null, json, guildId);
    }

    public Category createCategory(GuildImpl guild, DataObject json, long guildId) {
        boolean playbackCache = false;
        long id = json.getLong("id");

        if (guild == null) {
            guild = (GuildImpl) getJDA().getGuildsView().get(guildId);
        }
        if (guild == null) {
            throw new IllegalStateException("Missing guild for id " + Long.toUnsignedString(guildId));
        }

        CategoryImpl channel = (CategoryImpl) guild.getCategoryById(id);
        if (channel == null) {
            ChannelCacheViewImpl<GuildChannel> guildView = guild.getChannelView();
            ChannelCacheViewImpl<Channel> globalView = getJDA().getChannelsView();
            try (UnlockHook glock = guildView.writeLock();
                    UnlockHook jlock = globalView.writeLock()) {
                channel = new CategoryImpl(id, guild);
                guildView.put(channel);
                playbackCache = globalView.put(channel) == null;
            }
        }

        configureCategory(json, channel);
        Optional<DataArray> permissionOverwrites = json.optArray("permission_overwrites");
        if (permissionOverwrites.isPresent()) {
            createOverridesPass(channel, permissionOverwrites.get());
        }
        if (playbackCache) {
            getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, id);
        }
        return channel;
    }

    public TextChannel createTextChannel(DataObject json, long guildId) {
        return createTextChannel(null, json, guildId);
    }

    public TextChannel createTextChannel(GuildImpl guildObj, DataObject json, long guildId) {
        boolean playbackCache = false;
        long id = json.getLong("id");

        if (guildObj == null) {
            guildObj = (GuildImpl) getJDA().getGuildsView().get(guildId);
        }
        if (guildObj == null) {
            throw new IllegalStateException("Missing guild for id " + Long.toUnsignedString(guildId));
        }

        TextChannelImpl channel = (TextChannelImpl) guildObj.getTextChannelById(id);
        if (channel == null) {
            ChannelCacheViewImpl<GuildChannel> guildView = guildObj.getChannelView();
            ChannelCacheViewImpl<Channel> globalView = getJDA().getChannelsView();
            try (UnlockHook glock = guildView.writeLock();
                    UnlockHook jlock = globalView.writeLock()) {
                channel = new TextChannelImpl(id, guildObj);
                guildView.put(channel);
                playbackCache = globalView.put(channel) == null;
            }
        }

        configureTextChannel(json, channel);
        Optional<DataArray> permissionOverwrites = json.optArray("permission_overwrites");
        if (permissionOverwrites.isPresent()) {
            createOverridesPass(channel, permissionOverwrites.get());
        }
        if (playbackCache) {
            getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, id);
        }
        return channel;
    }

    public NewsChannel createNewsChannel(DataObject json, long guildId) {
        return createNewsChannel(null, json, guildId);
    }

    public NewsChannel createNewsChannel(GuildImpl guildObj, DataObject json, long guildId) {
        boolean playbackCache = false;
        long id = json.getLong("id");

        if (guildObj == null) {
            guildObj = (GuildImpl) getJDA().getGuildsView().get(guildId);
        }
        if (guildObj == null) {
            throw new IllegalStateException("Missing guild for id " + Long.toUnsignedString(guildId));
        }

        NewsChannelImpl channel = (NewsChannelImpl) guildObj.getNewsChannelById(id);
        if (channel == null) {
            ChannelCacheViewImpl<GuildChannel> guildView = guildObj.getChannelView();
            ChannelCacheViewImpl<Channel> globalView = getJDA().getChannelsView();
            try (UnlockHook glock = guildView.writeLock();
                    UnlockHook jlock = globalView.writeLock()) {
                channel = new NewsChannelImpl(id, guildObj);
                guildView.put(channel);
                playbackCache = globalView.put(channel) == null;
            }
        }

        configureNewsChannel(json, channel);
        Optional<DataArray> permissionOverwrites = json.optArray("permission_overwrites");
        if (permissionOverwrites.isPresent()) {
            createOverridesPass(channel, permissionOverwrites.get());
        }
        if (playbackCache) {
            getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, id);
        }
        return channel;
    }

    public VoiceChannel createVoiceChannel(DataObject json, long guildId) {
        return createVoiceChannel(null, json, guildId);
    }

    public VoiceChannel createVoiceChannel(GuildImpl guild, DataObject json, long guildId) {
        boolean playbackCache = false;
        long id = json.getLong("id");

        if (guild == null) {
            guild = (GuildImpl) getJDA().getGuildsView().get(guildId);
        }
        if (guild == null) {
            throw new IllegalStateException("Missing guild for id " + Long.toUnsignedString(guildId));
        }

        VoiceChannelImpl channel = (VoiceChannelImpl) guild.getVoiceChannelById(id);
        if (channel == null) {
            ChannelCacheViewImpl<GuildChannel> guildView = guild.getChannelView();
            ChannelCacheViewImpl<Channel> globalView = getJDA().getChannelsView();
            try (UnlockHook glock = guildView.writeLock();
                    UnlockHook jlock = globalView.writeLock()) {
                channel = new VoiceChannelImpl(id, guild);
                guildView.put(channel);
                playbackCache = globalView.put(channel) == null;
            }
        }

        configureVoiceChannel(json, channel);
        Optional<DataArray> permissionOverwrites = json.optArray("permission_overwrites");
        if (permissionOverwrites.isPresent()) {
            createOverridesPass(channel, permissionOverwrites.get());
        }
        if (playbackCache) {
            getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, id);
        }
        return channel;
    }

    public StageChannel createStageChannel(DataObject json, long guildId) {
        return createStageChannel(null, json, guildId);
    }

    public StageChannel createStageChannel(GuildImpl guild, DataObject json, long guildId) {
        boolean playbackCache = false;
        long id = json.getLong("id");

        if (guild == null) {
            guild = (GuildImpl) getJDA().getGuildsView().get(guildId);
        }
        if (guild == null) {
            throw new IllegalStateException("Missing guild for id " + Long.toUnsignedString(guildId));
        }

        StageChannelImpl channel = (StageChannelImpl) guild.getStageChannelById(id);
        if (channel == null) {
            ChannelCacheViewImpl<GuildChannel> guildView = guild.getChannelView();
            ChannelCacheViewImpl<Channel> globalView = getJDA().getChannelsView();
            try (UnlockHook glock = guildView.writeLock();
                    UnlockHook jlock = globalView.writeLock()) {
                channel = new StageChannelImpl(id, guild);
                guildView.put(channel);
                playbackCache = globalView.put(channel) == null;
            }
        }

        configureStageChannel(json, channel);
        Optional<DataArray> permissionOverwrites = json.optArray("permission_overwrites");
        if (permissionOverwrites.isPresent()) {
            createOverridesPass(channel, permissionOverwrites.get());
        }
        if (playbackCache) {
            getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, id);
        }
        return channel;
    }

    public ThreadChannel createThreadChannel(DataObject json, long guildId) {
        return createThreadChannel(null, json, guildId);
    }

    public ThreadChannel createThreadChannel(GuildImpl guild, DataObject json, long guildId) {
        return createThreadChannel(guild, json, guildId, true);
    }

    public ThreadChannel createThreadChannel(GuildImpl guild, DataObject json, long guildId, boolean modifyCache) {
        boolean playbackCache = false;
        long id = json.getUnsignedLong("id");
        long parentId = json.getUnsignedLong("parent_id");

        if (guild == null) {
            guild = (GuildImpl) getJDA().getGuildsView().get(guildId);
        }
        if (guild == null) {
            throw new IllegalStateException("Missing guild for id " + Long.toUnsignedString(guildId));
        }

        IThreadContainer parent = guild.getChannelById(IThreadContainer.class, parentId);
        if (parent == null) {
            throw new IllegalArgumentException(MISSING_CHANNEL);
        }

        ThreadChannelImpl channel = ((ThreadChannelImpl) getJDA().getThreadChannelById(id));
        if (channel == null) {
            ChannelCacheViewImpl<GuildChannel> guildThreadView = guild.getChannelView();
            ChannelCacheViewImpl<Channel> threadView = getJDA().getChannelsView();
            try (UnlockHook vlock = guildThreadView.writeLock();
                    UnlockHook jlock = threadView.writeLock()) {
                ChannelType type = ChannelType.fromId(json.getInt("type"));
                channel = new ThreadChannelImpl(id, guild, type);
                if (modifyCache) {
                    guildThreadView.put(channel);
                    playbackCache = threadView.put(channel) == null;
                }
            }
        }

        configureThreadChannel(json, channel);

        if (!json.isNull("applied_tags") && api.isCacheFlagSet(CacheFlag.FORUM_TAGS)) {
            DataArray array = json.getArray("applied_tags");
            channel.setAppliedTags(IntStream.range(0, array.length()).mapToLong(array::getUnsignedLong));
        }

        channel.setParentChannel(parent);

        // If the bot in the thread already, then create a thread member for the bot.
        if (!json.isNull("member")) {
            ThreadMember selfThreadMember =
                    createThreadMember(channel, guild.getSelfMember(), json.getObject("member"));
            CacheView.SimpleCacheView<ThreadMember> view = channel.getThreadMemberView();
            try (UnlockHook lock = view.writeLock()) {
                view.getMap().put(selfThreadMember.getIdLong(), selfThreadMember);
            }
        }

        if (playbackCache) {
            getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, id);
        }
        return channel;
    }

    public ThreadMember createThreadMember(GuildImpl guild, ThreadChannelImpl threadChannel, DataObject json) {
        DataObject memberJson = json.getObject("member");
        DataObject presenceJson = json.isNull("presence") ? null : json.getObject("presence");

        Member member = createMember(guild, memberJson, null, presenceJson);
        return createThreadMember(threadChannel, member, json);
    }

    public ThreadMember createThreadMember(ThreadChannelImpl threadChannel, Member member, DataObject json) {
        ThreadMemberImpl threadMember = new ThreadMemberImpl(member, threadChannel);
        threadMember.setJoinedTimestamp(Helpers.toTimestamp(json.getString("join_timestamp")));

        return threadMember;
    }

    public ForumChannel createForumChannel(DataObject json, long guildId) {
        return createForumChannel(null, json, guildId);
    }

    public ForumChannel createForumChannel(GuildImpl guild, DataObject json, long guildId) {
        boolean playbackCache = false;
        long id = json.getLong("id");

        if (guild == null) {
            guild = (GuildImpl) getJDA().getGuildsView().get(guildId);
        }
        if (guild == null) {
            throw new IllegalStateException("Missing guild for id " + Long.toUnsignedString(guildId));
        }

        ForumChannelImpl channel = (ForumChannelImpl) guild.getForumChannelById(id);
        if (channel == null) {
            ChannelCacheViewImpl<GuildChannel> guildView = guild.getChannelView();
            ChannelCacheViewImpl<Channel> globalView = getJDA().getChannelsView();
            try (UnlockHook glock = guildView.writeLock();
                    UnlockHook jlock = globalView.writeLock()) {
                channel = new ForumChannelImpl(id, guild);
                guildView.put(channel);
                playbackCache = globalView.put(channel) == null;
            }
        }
        configureForumChannel(json, channel);
        Optional<DataArray> permissionOverwrites = json.optArray("permission_overwrites");
        if (permissionOverwrites.isPresent()) {
            createOverridesPass(channel, permissionOverwrites.get());
        }
        if (playbackCache) {
            getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, id);
        }
        return channel;
    }

    public MediaChannel createMediaChannel(DataObject json, long guildId) {
        return createMediaChannel(null, json, guildId);
    }

    public MediaChannel createMediaChannel(GuildImpl guild, DataObject json, long guildId) {
        boolean playbackCache = false;
        long id = json.getLong("id");

        if (guild == null) {
            guild = (GuildImpl) getJDA().getGuildsView().get(guildId);
        }
        if (guild == null) {
            throw new IllegalStateException("Missing guild for id " + Long.toUnsignedString(guildId));
        }

        MediaChannelImpl channel = (MediaChannelImpl) guild.getMediaChannelById(id);
        if (channel == null) {
            ChannelCacheViewImpl<GuildChannel> guildView = guild.getChannelView();
            ChannelCacheViewImpl<Channel> globalView = getJDA().getChannelsView();
            try (UnlockHook glock = guildView.writeLock();
                    UnlockHook jlock = globalView.writeLock()) {
                channel = new MediaChannelImpl(id, guild);
                guildView.put(channel);
                playbackCache = globalView.put(channel) == null;
            }
        }
        configureMediaChannel(json, channel);
        Optional<DataArray> permissionOverwrites = json.optArray("permission_overwrites");
        if (permissionOverwrites.isPresent()) {
            createOverridesPass(channel, permissionOverwrites.get());
        }
        if (playbackCache) {
            getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, id);
        }
        return channel;
    }

    public PrivateChannel createPrivateChannel(DataObject json) {
        return createPrivateChannel(json, null);
    }

    public PrivateChannel createPrivateChannel(DataObject json, UserImpl user) {
        long channelId = json.getUnsignedLong("id");
        PrivateChannelImpl channel = (PrivateChannelImpl) api.getPrivateChannelById(channelId);
        if (channel == null) {
            channel = new PrivateChannelImpl(getJDA(), channelId, user);
        }
        configurePrivateChannel(json, channel);
        UserImpl recipient = user;
        if (channel.getUser() == null) {
            if (recipient == null && (json.hasKey("recipients") || json.hasKey("recipient"))) {
                // if we don't know the recipient, and we have information on them, we can use that
                DataObject recipientJson = json.hasKey("recipients")
                        ? json.getArray("recipients").getObject(0)
                        : json.getObject("recipient");
                long userId = recipientJson.getUnsignedLong("id");
                recipient = (UserImpl) getJDA().getUserById(userId);
                if (recipient == null) {
                    recipient = createUser(recipientJson);
                }
            }
            if (recipient != null) {
                // update the channel if we have found the user
                channel.setUser(recipient);
            }
        }
        if (recipient != null) {
            recipient.setPrivateChannel(channel);
        }
        // only add channels to the cache when they come from an event,
        // otherwise we would never remove the channel
        cachePrivateChannel(channel);
        api.usedPrivateChannel(channelId);
        return channel;
    }

    private void cachePrivateChannel(PrivateChannelImpl priv) {
        ChannelCacheViewImpl<Channel> privateView = getJDA().getChannelsView();
        privateView.put(priv);
        api.usedPrivateChannel(priv.getIdLong());
        getJDA().getEventCache().playbackCache(EventCache.Type.CHANNEL, priv.getIdLong());
    }

    @Nullable
    public StageInstance createStageInstance(GuildImpl guild, DataObject json) {
        long channelId = json.getUnsignedLong("channel_id");
        StageChannelImpl channel = (StageChannelImpl) guild.getStageChannelById(channelId);
        if (channel == null) {
            return null;
        }

        long id = json.getUnsignedLong("id");
        String topic = json.getString("topic");
        StageInstance.PrivacyLevel level = StageInstance.PrivacyLevel.fromKey(json.getInt("privacy_level", -1));

        StageInstanceImpl instance = (StageInstanceImpl) channel.getStageInstance();
        if (instance == null) {
            instance = new StageInstanceImpl(id, channel);
            channel.setStageInstance(instance);
        }

        return instance.setPrivacyLevel(level).setTopic(topic);
    }

    public void createOverridesPass(IPermissionContainerMixin<?> channel, DataArray overrides) {
        for (int i = 0; i < overrides.length(); i++) {
            try {
                createPermissionOverride(overrides.getObject(i), channel);
            } catch (NoSuchElementException e) {
                // Caused by Discord not properly clearing PermissionOverrides
                // when a Member leaves a Guild.
                LOG.debug("{}. Ignoring PermissionOverride.", e.getMessage());
            } catch (IllegalArgumentException e) {
                // Missing handling for a type
                LOG.warn("{}. Ignoring PermissionOverride.", e.getMessage());
            }
        }
    }

    public Role createRole(GuildImpl guild, DataObject roleJson, long guildId) {
        boolean playbackCache = false;
        long id = roleJson.getLong("id");
        if (guild == null) {
            guild = (GuildImpl) getJDA().getGuildsView().get(guildId);
        }
        RoleImpl role = (RoleImpl) guild.getRolesView().get(id);
        if (role == null) {
            SnowflakeCacheViewImpl<Role> roleView = guild.getRolesView();
            try (UnlockHook hook = roleView.writeLock()) {
                role = new RoleImpl(id, guild);
                playbackCache = roleView.getMap().put(id, role) == null;
            }
        }
        configureRole(roleJson, role, id);

        if (playbackCache) {
            getJDA().getEventCache().playbackCache(EventCache.Type.ROLE, id);
        }
        return role;
    }

    public ReceivedMessage createMessageBestEffort(DataObject json, MessageChannel channel, Guild guild) {
        if (channel != null) {
            return createMessageWithChannel(json, channel, false);
        } else {
            return createMessageFromWebhook(json, guild);
        }
    }

    public ReceivedMessage createMessageFromWebhook(DataObject json, @Nullable Guild guild) {
        return createMessage0(json, null, (GuildImpl) guild, false);
    }

    public ReceivedMessage createMessageWithChannel(
            DataObject json, @Nonnull MessageChannel channel, boolean modifyCache) {
        // Use channel directly if message is from a known guild channel
        if (channel instanceof GuildMessageChannel) {
            GuildMessageChannel messageChannel = (GuildMessageChannel) channel;
            return createMessage0(
                    json,
                    channel,
                    messageChannel.isDetached() ? null : (GuildImpl) messageChannel.getGuild(),
                    modifyCache);
        }
        if (channel instanceof GroupChannel) {
            return createMessage0(json, channel, null, modifyCache);
        }

        // Try to resolve private channel recipient if needed
        if (channel instanceof PrivateChannel) {
            return createMessageWithLookup(json, null, modifyCache);
        }
        throw new IllegalArgumentException(MISSING_CHANNEL);
    }

    public ReceivedMessage createMessageWithLookup(DataObject json, @Nullable Guild guild, boolean modifyCache) {
        // Private channels may be partial in our cache and missing recipient information
        // we can try and derive the user from the message here
        if (guild == null) {
            return createMessage0(json, createPrivateChannelByMessage(json), null, modifyCache);
        }
        // If we know that the message was sent in a guild,
        // we can use the guild to resolve the channel directly
        MessageChannel channel = guild.getChannelById(GuildMessageChannel.class, json.getUnsignedLong("channel_id"));
        //        if (channel == null)
        //            throw new IllegalArgumentException(MISSING_CHANNEL);
        return createMessage0(json, channel, (GuildImpl) guild, modifyCache);
    }

    // This tries to build a private channel instance through an arbitrary message object
    private PrivateChannel createPrivateChannelByMessage(DataObject message) {
        long channelId = message.getLong("channel_id");
        DataObject author = message.getObject("author");

        PrivateChannelImpl channel = (PrivateChannelImpl) getJDA().getPrivateChannelById(channelId);
        boolean isRecipient = !author.getBoolean("bot"); // bots cannot dm other bots
        if (channel == null) {
            DataObject channelData = DataObject.empty().put("id", channelId);

            // if we see an author that isn't us,
            // we can assume that is the other side of this private channel
            // if the author is us, we learn no information about the user at the other end
            if (isRecipient) {
                channelData.put("recipient", author);
            }

            // even without knowing the user at the other end,
            // we can still construct a minimal channel
            channel = (PrivateChannelImpl) createPrivateChannel(channelData);
        } else if (channel.getUser() == null && isRecipient) {
            // In this situation, we already know the channel
            // but the message provided us with the recipient
            // which we can now add to the channel
            UserImpl user = createUser(author);
            channel.setUser(user);
            user.setPrivateChannel(channel);
        }

        return channel;
    }

    private ReceivedMessage createMessage0(
            DataObject jsonObject, @Nullable MessageChannel channel, @Nullable GuildImpl guild, boolean modifyCache) {
        MessageType type = MessageType.fromId(jsonObject.getInt("type"));
        if (type == MessageType.UNKNOWN) {
            throw new IllegalArgumentException(UNKNOWN_MESSAGE_TYPE);
        }

        long id = jsonObject.getLong("id");
        DataObject author = jsonObject.getObject("author");
        long authorId = author.getLong("id");
        long channelId = jsonObject.getUnsignedLong("channel_id");
        long guildId = channel instanceof GuildChannel
                ? ((GuildChannel) channel).getGuild().getIdLong()
                : jsonObject.getUnsignedLong("guild_id", 0L);
        MemberImpl member = null;

        // Member details for author
        if (guild != null && !jsonObject.isNull("member")) {
            DataObject memberJson = jsonObject.getObject("member");
            memberJson.put("user", author);
            member = createMember(guild, memberJson);
            if (modifyCache) {
                // Update member cache with new information if needed
                updateMemberCache(member);
            }
        }

        String content = jsonObject.getString("content", "");
        boolean fromWebhook = jsonObject.hasKey("webhook_id");
        long applicationId = jsonObject.getUnsignedLong("application_id", 0);
        boolean pinned = jsonObject.getBoolean("pinned");
        boolean tts = jsonObject.getBoolean("tts");
        boolean mentionsEveryone = jsonObject.getBoolean("mention_everyone");
        OffsetDateTime editTime = jsonObject.isNull("edited_timestamp")
                ? null
                : OffsetDateTime.parse(jsonObject.getString("edited_timestamp"));
        String nonce =
                jsonObject.isNull("nonce") ? null : jsonObject.get("nonce").toString();
        int flags = jsonObject.getInt("flags", 0);

        // Message accessories
        MessageChannel tmpChannel = channel; // because java
        List<Message.Attachment> attachments = map(jsonObject, "attachments", this::createMessageAttachment);
        List<MessageEmbed> embeds = map(jsonObject, "embeds", this::createMessageEmbed);
        List<MessageReaction> reactions =
                map(jsonObject, "reactions", (obj) -> createMessageReaction(tmpChannel, channelId, id, obj));
        List<StickerItem> stickers = map(jsonObject, "sticker_items", this::createStickerItem);
        // Keep the unknown components so the user can read them if they want
        List<MessageTopLevelComponentUnion> components = map(
                jsonObject,
                "components",
                (obj) -> DEFAULT_COMPONENT_DESERIALIZER.deserializeAs(MessageTopLevelComponentUnion.class, obj));

        MessagePoll poll = jsonObject
                .optObject("poll")
                .map(EntityBuilder::createMessagePoll)
                .orElse(null);

        // Message activity (for game invites/spotify)
        MessageActivity activity = null;
        if (!jsonObject.isNull("activity")) {
            activity = createMessageActivity(jsonObject);
        }

        // Message Author
        User user;
        if (guild != null) {
            if (member == null) {
                member = (MemberImpl) guild.getMemberById(authorId);
            }
            user = member != null ? member.getUser() : null;
            if (user == null) {
                if (fromWebhook || !modifyCache) {
                    user = createUser(author); // Specifically for MESSAGE_CREATE
                } else {
                    throw new IllegalArgumentException(MISSING_USER); // Specifically for MESSAGE_CREATE
                }
            }
        } else if (channel instanceof PrivateChannel) {
            // Assume private channel
            if (authorId == getJDA().getSelfUser().getIdLong()) {
                user = getJDA().getSelfUser();
            } else {
                // PrivateChannel.getUser() can produce null, see docs
                user = ((PrivateChannel) channel).getUser();
                if (user == null) {
                    user = createUser(author);
                }
            }
        } else {
            user = createUser(author);
        }

        if (modifyCache && !fromWebhook) { // update the user information on message receive
            updateUser((UserImpl) user, author);
        }

        // Message Reference (Reply or Pin)
        Message referencedMessage = null;
        if (!jsonObject.isNull("referenced_message")) {
            DataObject referenceJson = jsonObject.getObject("referenced_message");
            try {
                referencedMessage = createMessage0(referenceJson, channel, guild, false);
            } catch (IllegalArgumentException ex) {
                // We can just discard the message for some trivial cases
                if (UNKNOWN_MESSAGE_TYPE.equals(ex.getMessage())) {
                    LOG.debug(
                            "Received referenced message with unknown type. Type: {}",
                            referenceJson.getInt("type", -1));
                } else if (MISSING_CHANNEL.equals(ex.getMessage())) {
                    LOG.debug(
                            "Received referenced message with unknown channel. channel_id: {} Type: {}",
                            referenceJson.getUnsignedLong("channel_id", 0),
                            referenceJson.getInt("type", -1));
                } else {
                    throw ex;
                }
            }
        }

        List<MessageSnapshot> snapshots = Collections.emptyList();
        MessageReference messageReference = null;

        if (!jsonObject.isNull("message_reference")) {
            DataObject messageReferenceJson = jsonObject.getObject("message_reference");

            messageReference = new MessageReference(
                    messageReferenceJson.getInt("type", -1),
                    messageReferenceJson.getLong("message_id", 0),
                    messageReferenceJson.getLong("channel_id", 0),
                    messageReferenceJson.getLong("guild_id", 0),
                    referencedMessage,
                    api);

            MessageReference finalReference = messageReference;
            snapshots = map(
                    jsonObject,
                    "message_snapshots",
                    (obj) -> createMessageSnapshot(finalReference, obj.getObject("message")));
        }

        // Application command and component replies
        @SuppressWarnings("deprecation")
        Message.Interaction messageInteraction = null;
        if (!jsonObject.isNull("interaction")) {
            messageInteraction = createMessageInteraction(guild, jsonObject.getObject("interaction"));
        }

        Message.InteractionMetadata interactionMetadata = null;
        if (!jsonObject.isNull("interaction_metadata")) {
            interactionMetadata = createMessageInteractionMetadata(jsonObject.getObject("interaction_metadata"));
        }

        // Lazy Mention parsing and caching (includes reply mentions)
        Mentions mentions = new MessageMentionsImpl(
                api,
                guild,
                content,
                mentionsEveryone,
                jsonObject.getArray("mentions"),
                jsonObject.getArray("mention_roles"));

        ThreadChannel startedThread = null;
        if (guild != null && !jsonObject.isNull("thread")) {
            startedThread = createThreadChannel(guild, jsonObject.getObject("thread"), guild.getIdLong());
        }

        int position = jsonObject.getInt("position", -1);

        return new ReceivedMessage(
                id,
                channelId,
                guildId,
                api,
                guild,
                channel,
                type,
                messageReference,
                fromWebhook,
                applicationId,
                tts,
                pinned,
                content,
                nonce,
                user,
                member,
                activity,
                poll,
                editTime,
                mentions,
                reactions,
                attachments,
                embeds,
                stickers,
                components,
                snapshots,
                flags,
                messageInteraction,
                interactionMetadata,
                startedThread,
                position);
    }

    private static MessageActivity createMessageActivity(DataObject jsonObject) {
        DataObject activityData = jsonObject.getObject("activity");
        MessageActivity.ActivityType activityType = MessageActivity.ActivityType.fromId(activityData.getInt("type"));
        String partyId = activityData.getString("party_id", null);
        MessageActivity.Application application = null;

        if (!jsonObject.isNull("application")) {
            DataObject applicationData = jsonObject.getObject("application");

            String name = applicationData.getString("name");
            String description = applicationData.getString("description", "");
            String iconId = applicationData.getString("icon", null);
            String coverId = applicationData.getString("cover_image", null);
            long applicationId = applicationData.getLong("id");

            application = new MessageActivity.Application(name, description, iconId, coverId, applicationId);
        }
        if (activityType == MessageActivity.ActivityType.UNKNOWN) {
            LOG.debug("Received an unknown ActivityType, Activity: {}", activityData);
        }

        return new MessageActivity(activityType, partyId, application);
    }

    public static MessagePollImpl createMessagePoll(DataObject data) {
        MessagePoll.LayoutType layout = MessagePoll.LayoutType.fromKey(data.getInt("layout_type"));
        OffsetDateTime expiresAt = data.isNull("expiry") ? null : data.getOffsetDateTime("expiry");
        boolean isMultiAnswer = data.getBoolean("allow_multiselect");

        DataArray answersData = data.getArray("answers");
        DataObject questionData = data.getObject("question");

        DataObject resultsData = data.optObject("results")
                .orElseGet(
                        () -> DataObject.empty().put("answer_counts", DataArray.empty()) // FIXME: Discord bug
                        );
        boolean isFinalized = resultsData.getBoolean("is_finalized");

        DataArray resultVotes = resultsData.getArray("answer_counts");
        TLongObjectMap<DataObject> voteMapping = new TLongObjectHashMap<>();
        resultVotes.stream(DataArray::getObject).forEach(votes -> voteMapping.put(votes.getLong("id"), votes));

        MessagePoll.Question question = new MessagePoll.Question(
                questionData.getString("text"),
                questionData.optObject("emoji").map(Emoji::fromData).orElse(null));

        List<MessagePoll.Answer> answers = answersData.stream(DataArray::getObject)
                .map(answer -> {
                    long answerId = answer.getLong("answer_id");
                    DataObject media = answer.getObject("poll_media");
                    DataObject votes = voteMapping.get(answerId);
                    return new MessagePoll.Answer(
                            answerId,
                            media.getString("text"),
                            media.optObject("emoji").map(Emoji::fromData).orElse(null),
                            votes != null ? votes.getInt("count") : 0,
                            votes != null && votes.getBoolean("me_voted"));
                })
                .collect(Helpers.toUnmodifiableList());

        return new MessagePollImpl(layout, question, answers, expiresAt, isMultiAnswer, isFinalized);
    }

    public MessageReaction createMessageReaction(MessageChannel chan, long channelId, long messageId, DataObject obj) {
        DataObject emoji = obj.getObject("emoji");
        int[] count = new int[] {
            obj.getInt("count", 0), // total
            obj.optObject("count_details").map(o -> o.getInt("normal", 0)).orElse(0),
            obj.optObject("count_details").map(o -> o.getInt("burst", 0)).orElse(0),
        };
        boolean[] me = new boolean[] {
            obj.getBoolean("me"), // normal
            obj.getBoolean("me_burst") // super
        };
        EmojiUnion emojiObj = createEmoji(emoji);

        return new MessageReaction(api, chan, emojiObj, channelId, messageId, me, count);
    }

    public Message.Attachment createMessageAttachment(DataObject jsonObject) {
        boolean ephemeral = jsonObject.getBoolean("ephemeral", false);
        int width = jsonObject.getInt("width", -1);
        int height = jsonObject.getInt("height", -1);
        int size = jsonObject.getInt("size");
        String url = jsonObject.getString("url");
        String proxyUrl = jsonObject.getString("proxy_url");
        String filename = jsonObject.getString("filename");
        String contentType = jsonObject.getString("content_type", null);
        String description = jsonObject.getString("description", null);
        long id = jsonObject.getLong("id");
        String waveform = jsonObject.getString("waveform", null);
        double duration = jsonObject.getDouble("duration_secs", 0);
        return new Message.Attachment(
                id,
                url,
                proxyUrl,
                filename,
                contentType,
                description,
                size,
                height,
                width,
                ephemeral,
                waveform,
                duration,
                getJDA());
    }

    public MessageEmbed createMessageEmbed(DataObject content) {
        if (content.isNull("type")) {
            throw new IllegalStateException(
                    "Encountered embed object with missing/null type field for Json: " + content);
        }
        EmbedType type = EmbedType.fromKey(content.getString("type"));
        String url = content.getString("url", null);
        String title = content.getString("title", null);
        String description = content.getString("description", null);
        OffsetDateTime timestamp =
                content.isNull("timestamp") ? null : OffsetDateTime.parse(content.getString("timestamp"));
        int color = content.isNull("color") ? Role.DEFAULT_COLOR_RAW : content.getInt("color");

        Thumbnail thumbnail;
        if (content.isNull("thumbnail")) {
            thumbnail = null;
        } else {
            DataObject obj = content.getObject("thumbnail");
            thumbnail = new Thumbnail(
                    obj.getString("url", null),
                    obj.getString("proxy_url", null),
                    obj.getInt("width", -1),
                    obj.getInt("height", -1));
        }

        Provider provider;
        if (content.isNull("provider")) {
            provider = null;
        } else {
            DataObject obj = content.getObject("provider");
            provider = new Provider(obj.getString("name", null), obj.getString("url", null));
        }

        AuthorInfo author;
        if (content.isNull("author")) {
            author = null;
        } else {
            DataObject obj = content.getObject("author");
            author = new AuthorInfo(
                    obj.getString("name", null),
                    obj.getString("url", null),
                    obj.getString("icon_url", null),
                    obj.getString("proxy_icon_url", null));
        }

        VideoInfo video;
        if (content.isNull("video")) {
            video = null;
        } else {
            DataObject obj = content.getObject("video");
            video = new VideoInfo(
                    obj.getString("url", null),
                    obj.getString("proxy_url", null),
                    obj.getInt("width", -1),
                    obj.getInt("height", -1));
        }

        Footer footer;
        if (content.isNull("footer")) {
            footer = null;
        } else {
            DataObject obj = content.getObject("footer");
            footer = new Footer(
                    obj.getString("text", null),
                    obj.getString("icon_url", null),
                    obj.getString("proxy_icon_url", null));
        }

        ImageInfo image;
        if (content.isNull("image")) {
            image = null;
        } else {
            DataObject obj = content.getObject("image");
            image = new ImageInfo(
                    obj.getString("url", null),
                    obj.getString("proxy_url", null),
                    obj.getInt("width", -1),
                    obj.getInt("height", -1));
        }

        List<Field> fields = map(
                content,
                "fields",
                (obj) -> new Field(
                        obj.getString("name", null), obj.getString("value", null), obj.getBoolean("inline"), false));

        return createMessageEmbed(
                url,
                title,
                description,
                type,
                timestamp,
                color,
                thumbnail,
                provider,
                author,
                video,
                footer,
                image,
                fields);
    }

    public static MessageEmbed createMessageEmbed(
            String url,
            String title,
            String description,
            EmbedType type,
            OffsetDateTime timestamp,
            int color,
            Thumbnail thumbnail,
            Provider siteProvider,
            AuthorInfo author,
            VideoInfo videoInfo,
            Footer footer,
            ImageInfo image,
            List<Field> fields) {
        return new MessageEmbed(
                url,
                title,
                description,
                type,
                timestamp,
                color,
                thumbnail,
                siteProvider,
                author,
                videoInfo,
                footer,
                image,
                fields);
    }

    public StickerItem createStickerItem(DataObject content) {
        long id = content.getLong("id");
        String name = content.getString("name");
        Sticker.StickerFormat format = Sticker.StickerFormat.fromId(content.getInt("format_type"));
        return new StickerItemImpl(id, format, name);
    }

    public RichStickerImpl createRichSticker(DataObject content) {
        long id = content.getLong("id");
        String name = content.getString("name");
        Sticker.StickerFormat format = Sticker.StickerFormat.fromId(content.getInt("format_type"));
        Sticker.Type type = Sticker.Type.fromId(content.getInt("type", -1));

        String description = content.getString("description", "");
        Set<String> tags = Collections.emptySet();
        if (!content.isNull("tags")) {
            String[] array = content.getString("tags").split(",\\s*");
            tags = Helpers.setOf(array);
        }

        switch (type) {
            case GUILD:
                boolean available = content.getBoolean("available");
                long guildId = content.getUnsignedLong("guild_id", 0L);
                User owner = content.isNull("user") ? null : createUser(content.getObject("user"));
                return new GuildStickerImpl(id, format, name, tags, description, available, guildId, api, owner);
            case STANDARD:
                long packId = content.getUnsignedLong("pack_id", 0L);
                int sortValue = content.getInt("sort_value", -1);
                return new StandardStickerImpl(id, format, name, tags, description, packId, sortValue);
            default:
                throw new IllegalArgumentException("Unknown sticker type. Type: " + type + " JSON: " + content);
        }
    }

    public StickerPack createStickerPack(DataObject content) {
        long id = content.getUnsignedLong("id");
        String name = content.getString("name");
        String description = content.getString("description", "");
        long skuId = content.getUnsignedLong("sku_id", 0);
        long coverId = content.getUnsignedLong("cover_sticker_id", 0);
        long bannerId = content.getUnsignedLong("banner_asset_id", 0);

        DataArray stickerArr = content.getArray("stickers");
        List<StandardSticker> stickers = new ArrayList<>(stickerArr.length());
        for (int i = 0; i < stickerArr.length(); i++) {
            DataObject object = null;
            try {
                object = stickerArr.getObject(i);
                StandardSticker sticker = (StandardSticker) createRichSticker(object);
                stickers.add(sticker);
            } catch (ParsingException | ClassCastException ex) {
                LOG.error("Sticker contained in pack {} ({}) could not be parsed. JSON: {}", name, id, object);
            }
        }

        return new StickerPackImpl(id, stickers, name, description, coverId, bannerId, skuId);
    }

    @SuppressWarnings("deprecation")
    public Message.Interaction createMessageInteraction(GuildImpl guildImpl, DataObject content) {
        long id = content.getLong("id");
        int type = content.getInt("type");
        String name = content.getString("name");
        DataObject userJson = content.getObject("user");
        User user = null;
        MemberImpl member = null;
        if (!content.isNull("member") && guildImpl != null) {
            DataObject memberJson = content.getObject("member");
            memberJson.put("user", userJson);
            member = createMember(guildImpl, memberJson);
            user = member.getUser();
        } else {
            user = createUser(userJson);
        }

        return new Message.Interaction(id, type, name, user, member);
    }

    public Message.InteractionMetadata createMessageInteractionMetadata(DataObject content) {
        long id = content.getLong("id");
        int type = content.getInt("type");
        User user = createUser(content.getObject("user"));
        IntegrationOwners integrationOwners =
                new IntegrationOwnersImpl(content.getObject("authorizing_integration_owners"));
        long originalResponseMessageId =
                content.isNull("original_response_message_id") ? 0 : content.getLong("original_response_message_id");
        long interactedMessageId =
                content.isNull("interacted_message_id") ? 0 : content.getLong("interacted_message_id");
        Message.InteractionMetadata triggeringInteraction = content.optObject("triggering_interaction_metadata")
                .map(this::createMessageInteractionMetadata)
                .orElse(null);
        User targetUser = content.optObject("target_user").map(this::createUser).orElse(null);
        long targetMessageId = content.isNull("target_message_id") ? 0 : content.getLong("target_message_id");

        return new Message.InteractionMetadata(
                id,
                type,
                user,
                integrationOwners,
                originalResponseMessageId,
                interactedMessageId,
                triggeringInteraction,
                targetUser,
                targetMessageId);
    }

    public MessageSnapshot createMessageSnapshot(MessageReference messageReference, DataObject jsonObject) {
        MessageType type = MessageType.fromId(jsonObject.getInt("type"));

        String content = jsonObject.getString("content", "");
        OffsetDateTime editTime = jsonObject.isNull("edited_timestamp")
                ? null
                : OffsetDateTime.parse(jsonObject.getString("edited_timestamp"));
        int flags = jsonObject.getInt("flags", 0);
        boolean mentionsEveryone = jsonObject.getBoolean("mention_everyone");

        List<Message.Attachment> attachments = map(jsonObject, "attachments", this::createMessageAttachment);
        List<MessageEmbed> embeds = map(jsonObject, "embeds", this::createMessageEmbed);
        List<StickerItem> stickers = map(jsonObject, "sticker_items", this::createStickerItem);
        // Keep the unknown components so the user can read them if they want
        List<MessageTopLevelComponentUnion> components = map(
                jsonObject,
                "components",
                (obj) -> DEFAULT_COMPONENT_DESERIALIZER.deserializeAs(MessageTopLevelComponentUnion.class, obj));

        Guild guild = messageReference.getGuild();
        // Lazy Mention parsing and caching (includes reply mentions)
        // This only works if the message is from the same guild
        Mentions mentions = new MessageMentionsImpl(
                api,
                guild instanceof GuildImpl ? (GuildImpl) guild : null,
                content,
                mentionsEveryone,
                jsonObject.getArray("mentions"),
                jsonObject.optArray("mention_roles").orElseGet(DataArray::empty));

        return new MessageSnapshot(type, mentions, editTime, content, attachments, embeds, components, stickers, flags);
    }

    @Nullable
    public PermissionOverride createPermissionOverride(DataObject override, IPermissionContainerMixin<?> chan) {
        int type = override.getInt("type");
        long id = override.getLong("id");
        boolean role = type == 0;
        if (role && chan.getGuild().getRoleById(id) == null) {
            throw new NoSuchElementException(
                    "Attempted to create a PermissionOverride for a non-existent role! JSON: " + override);
        }
        if (!role && type != 1) {
            throw new IllegalArgumentException("Provided with an unknown PermissionOverride type! JSON: " + override);
        }
        if (!role && id != api.getSelfUser().getIdLong() && !api.isCacheFlagSet(CacheFlag.MEMBER_OVERRIDES)) {
            return null;
        }

        long allow = override.getLong("allow");
        long deny = override.getLong("deny");
        // Don't cache empty @everyone overrides, they ruin our sync check
        if (id == chan.getGuild().getIdLong() && (allow | deny) == 0L) {
            return null;
        }

        PermissionOverrideImpl permOverride =
                (PermissionOverrideImpl) chan.getPermissionOverrideMap().get(id);
        if (permOverride == null) {
            permOverride = new PermissionOverrideImpl(chan, id, role);
            chan.getPermissionOverrideMap().put(id, permOverride);
        }

        return permOverride.setAllow(allow).setDeny(deny);
    }

    public WebhookImpl createWebhook(DataObject object) {
        return createWebhook(object, false);
    }

    public WebhookImpl createWebhook(DataObject object, boolean allowMissingChannel) {
        long id = object.getLong("id");
        long guildId = object.getUnsignedLong("guild_id");
        long channelId = object.getUnsignedLong("channel_id");
        String token = object.getString("token", null);
        WebhookType type = WebhookType.fromKey(object.getInt("type", -1));

        IWebhookContainer channel = getJDA().getChannelById(IWebhookContainer.class, channelId);
        if (channel == null && !allowMissingChannel) {
            throw new NullPointerException(String.format(
                    "Tried to create Webhook for an un-cached IWebhookContainer channel! WebhookId: %s ChannelId: %s GuildId: %s",
                    id, channelId, guildId));
        }

        Object name = !object.isNull("name") ? object.get("name") : null;
        Object avatar = !object.isNull("avatar") ? object.get("avatar") : null;

        DataObject fakeUser = DataObject.empty()
                .put("username", name)
                .put("discriminator", "0000")
                .put("id", id)
                .put("avatar", avatar);
        User defaultUser = createUser(fakeUser);

        Optional<DataObject> ownerJson = object.optObject("user");
        User owner = null;

        if (ownerJson.isPresent()) {
            DataObject json = ownerJson.get();
            long userId = json.getLong("id");

            owner = getJDA().getUserById(userId);
            if (owner == null) {
                json.put("id", userId);
                owner = createUser(json);
            }
        }

        Member ownerMember =
                owner == null || channel == null ? null : channel.getGuild().getMember(owner);
        WebhookImpl webhook = new WebhookImpl(channel, getJDA(), id, type)
                .setToken(token)
                .setOwner(ownerMember, owner)
                .setUser(defaultUser);

        if (!object.isNull("source_channel")) {
            DataObject source = object.getObject("source_channel");
            webhook.setSourceChannel(
                    new Webhook.ChannelReference(source.getUnsignedLong("id"), source.getString("name")));
        }
        if (!object.isNull("source_guild")) {
            DataObject source = object.getObject("source_guild");
            webhook.setSourceGuild(new Webhook.GuildReference(source.getUnsignedLong("id"), source.getString("name")));
        }

        return webhook;
    }

    public Invite createInvite(DataObject object) {
        String code = object.getString("code");
        User inviter = object.hasKey("inviter") ? createUser(object.getObject("inviter")) : null;

        DataObject channelObject = object.getObject("channel");
        ChannelType channelType = ChannelType.fromId(channelObject.getInt("type"));
        Invite.TargetType targetType = Invite.TargetType.fromId(object.getInt("target_type", 0));

        Invite.InviteType type;
        Invite.Guild guild;
        Invite.Channel channel;
        Invite.Group group;
        Invite.InviteTarget target;

        if (channelType == ChannelType.GROUP) {
            type = Invite.InviteType.GROUP;
            guild = null;
            channel = null;

            String groupName = channelObject.getString("name", "");
            long groupId = channelObject.getLong("id");
            String groupIconId = channelObject.getString("icon", null);

            List<String> usernames;
            if (channelObject.isNull("recipients")) {
                usernames = null;
            } else {
                usernames = map(channelObject, "recipients", (json) -> json.getString("username"));
            }

            group = new InviteImpl.GroupImpl(groupIconId, groupName, groupId, usernames);
        } else if (channelType.isGuild()) {
            type = Invite.InviteType.GUILD;

            DataObject guildObject = object.getObject("guild");

            String guildVanityCode = guildObject.getString("vanity_url_code", null);
            String guildBannerId = guildObject.getString("banner", null);
            String guildIconId = guildObject.getString("icon", null);
            long guildId = guildObject.getLong("id");
            String guildName = guildObject.getString("name");
            String guildSplashId = guildObject.getString("splash", null);
            String description = guildObject.getString("description", null);
            VerificationLevel guildVerificationLevel =
                    VerificationLevel.fromKey(guildObject.getInt("verification_level", -1));
            Guild.NSFWLevel guildNsfwLevel = Guild.NSFWLevel.fromKey(guildObject.getInt("nsfw_level", -1));
            int presenceCount = object.getInt("approximate_presence_count", -1);
            int memberCount = object.getInt("approximate_member_count", -1);

            Set<String> guildFeatures;
            if (guildObject.isNull("features")) {
                guildFeatures = Collections.emptySet();
            } else {
                guildFeatures = Collections.unmodifiableSet(
                        StreamSupport.stream(guildObject.getArray("features").spliterator(), false)
                                .map(String::valueOf)
                                .collect(Collectors.toSet()));
            }

            GuildWelcomeScreen welcomeScreen = guildObject.isNull("welcome_screen")
                    ? null
                    : createWelcomeScreen(null, guildObject.getObject("welcome_screen"));

            guild = new InviteImpl.GuildImpl(
                    guildId,
                    guildVanityCode,
                    guildBannerId,
                    guildIconId,
                    guildName,
                    guildSplashId,
                    description,
                    guildVerificationLevel,
                    guildNsfwLevel,
                    presenceCount,
                    memberCount,
                    guildFeatures,
                    welcomeScreen);

            String channelName = channelObject.getString("name");
            long channelId = channelObject.getLong("id");

            channel = new InviteImpl.ChannelImpl(channelId, channelName, channelType);
            group = null;
        } else {
            // Unknown channel type for invites

            type = Invite.InviteType.UNKNOWN;
            guild = null;
            channel = null;
            group = null;
        }

        switch (targetType) {
            case EMBEDDED_APPLICATION:
                DataObject applicationObject = object.getObject("target_application");

                Invite.EmbeddedApplication application = new InviteImpl.EmbeddedApplicationImpl(
                        applicationObject.getString("icon", null),
                        applicationObject.getString("name"),
                        applicationObject.getString("description"),
                        applicationObject.getString("summary"),
                        applicationObject.getLong("id"),
                        applicationObject.getInt("max_participants", -1));
                target = new InviteImpl.InviteTargetImpl(targetType, application, null);
                break;
            case STREAM:
                DataObject targetUserObject = object.getObject("target_user");
                target = new InviteImpl.InviteTargetImpl(targetType, null, createUser(targetUserObject));
                break;
            case NONE:
                target = null;
                break;
            default:
                target = new InviteImpl.InviteTargetImpl(targetType, null, null);
        }

        int maxAge;
        int maxUses;
        boolean temporary;
        OffsetDateTime timeCreated;
        int uses;
        boolean expanded;

        if (object.hasKey("max_uses")) {
            expanded = true;
            maxAge = object.getInt("max_age");
            maxUses = object.getInt("max_uses");
            uses = object.getInt("uses");
            temporary = object.getBoolean("temporary");
            timeCreated = OffsetDateTime.parse(object.getString("created_at"));
        } else {
            expanded = false;
            maxAge = -1;
            maxUses = -1;
            uses = -1;
            temporary = false;
            timeCreated = null;
        }

        return new InviteImpl(
                getJDA(),
                code,
                expanded,
                inviter,
                maxAge,
                maxUses,
                temporary,
                timeCreated,
                uses,
                channel,
                guild,
                group,
                target,
                type);
    }

    public GuildWelcomeScreen createWelcomeScreen(Guild guild, DataObject object) {
        DataArray welcomeChannelsArray = object.getArray("welcome_channels");
        List<GuildWelcomeScreen.Channel> welcomeChannels = new ArrayList<>(welcomeChannelsArray.length());
        for (int i = 0; i < welcomeChannelsArray.length(); i++) {
            DataObject welcomeChannelObj = welcomeChannelsArray.getObject(i);
            EmojiUnion emoji = null;
            if (!welcomeChannelObj.isNull("emoji_id") || !welcomeChannelObj.isNull("emoji_name")) {
                emoji = createEmoji(welcomeChannelObj, "emoji_name", "emoji_id");
            }

            welcomeChannels.add(new GuildWelcomeScreenImpl.ChannelImpl(
                    guild, welcomeChannelObj.getLong("channel_id"), welcomeChannelObj.getString("description"), emoji));
        }
        return new GuildWelcomeScreenImpl(
                guild, object.getString("description", null), Collections.unmodifiableList(welcomeChannels));
    }

    public Template createTemplate(DataObject object) {
        String code = object.getString("code");
        String name = object.getString("name");
        String description = object.getString("description", null);
        int uses = object.getInt("usage_count");
        User creator = createUser(object.getObject("creator"));
        OffsetDateTime createdAt = OffsetDateTime.parse(object.getString("created_at"));
        OffsetDateTime updatedAt = OffsetDateTime.parse(object.getString("updated_at"));
        long guildId = object.getLong("source_guild_id");
        DataObject guildObject = object.getObject("serialized_source_guild");
        String guildName = guildObject.getString("name");
        String guildDescription = guildObject.getString("description", null);
        String guildIconId = guildObject.getString("icon_hash", null);
        VerificationLevel guildVerificationLevel =
                VerificationLevel.fromKey(guildObject.getInt("verification_level", -1));
        NotificationLevel notificationLevel =
                NotificationLevel.fromKey(guildObject.getInt("default_message_notifications", 0));
        ExplicitContentLevel explicitContentLevel =
                ExplicitContentLevel.fromKey(guildObject.getInt("explicit_content_filter", 0));
        DiscordLocale locale = DiscordLocale.from(guildObject.getString("preferred_locale", "en-US"));
        Timeout afkTimeout = Timeout.fromKey(guildObject.getInt("afk_timeout", 0));
        DataArray roleArray = guildObject.getArray("roles");
        DataArray channelsArray = guildObject.getArray("channels");
        long afkChannelId = guildObject.getLong("afk_channel_id", -1L);
        long systemChannelId = guildObject.getLong("system_channel_id", -1L);

        List<TemplateRole> roles = new ArrayList<>();
        for (int i = 0; i < roleArray.length(); i++) {
            DataObject obj = roleArray.getObject(i);
            long roleId = obj.getLong("id");
            String roleName = obj.getString("name");
            int roleColor = obj.getInt("color");
            boolean hoisted = obj.getBoolean("hoist");
            boolean mentionable = obj.getBoolean("mentionable");
            long rawPermissions = obj.getLong("permissions");
            roles.add(new TemplateRole(
                    roleId,
                    roleName,
                    roleColor == 0 ? Role.DEFAULT_COLOR_RAW : roleColor,
                    hoisted,
                    mentionable,
                    rawPermissions));
        }

        List<TemplateChannel> channels = new ArrayList<>();
        for (int i = 0; i < channelsArray.length(); i++) {
            DataObject obj = channelsArray.getObject(i);
            long channelId = obj.getLong("id");
            int type = obj.getInt("type");
            ChannelType channelType = ChannelType.fromId(type);
            String channelName = obj.getString("name");
            String topic = obj.getString("topic", null);
            int rawPosition = obj.getInt("position");
            long parentId = obj.getLong("parent_id", -1);

            boolean nsfw = obj.getBoolean("nsfw");
            int slowmode = obj.getInt("rate_limit_per_user");

            int bitrate = obj.getInt("bitrate");
            int userLimit = obj.getInt("user_limit");

            List<TemplateChannel.PermissionOverride> permissionOverrides = new ArrayList<>();
            DataArray overrides = obj.getArray("permission_overwrites");

            for (int j = 0; j < overrides.length(); j++) {
                DataObject overrideObj = overrides.getObject(j);
                long overrideId = overrideObj.getLong("id");
                long allow = overrideObj.getLong("allow");
                long deny = overrideObj.getLong("deny");
                permissionOverrides.add(new TemplateChannel.PermissionOverride(overrideId, allow, deny));
            }
            channels.add(new TemplateChannel(
                    channelId,
                    channelType,
                    channelName,
                    topic,
                    rawPosition,
                    parentId,
                    type == 5,
                    permissionOverrides,
                    nsfw,
                    slowmode,
                    bitrate,
                    userLimit));
        }

        TemplateChannel afkChannel = channels.stream()
                .filter(templateChannel -> templateChannel.getIdLong() == afkChannelId)
                .findFirst()
                .orElse(null);
        TemplateChannel systemChannel = channels.stream()
                .filter(templateChannel -> templateChannel.getIdLong() == systemChannelId)
                .findFirst()
                .orElse(null);

        TemplateGuild guild = new TemplateGuild(
                guildId,
                guildName,
                guildDescription,
                guildIconId,
                guildVerificationLevel,
                notificationLevel,
                explicitContentLevel,
                locale,
                afkTimeout,
                afkChannel,
                systemChannel,
                roles,
                channels);

        boolean synced = !object.getBoolean("is_dirty", false);

        return new Template(getJDA(), code, name, description, uses, creator, createdAt, updatedAt, guild, synced);
    }

    public ApplicationInfo createApplicationInfo(DataObject object) {
        String description = object.getString("description");
        String termsOfServiceUrl = object.getString("terms_of_service_url", null);
        String privacyPolicyUrl = object.getString("privacy_policy_url", null);
        boolean doesBotRequireCodeGrant = object.getBoolean("bot_require_code_grant");
        String iconId = object.getString("icon", null);
        long id = object.getUnsignedLong("id");
        long flags = object.getUnsignedLong("flags", 0);
        String name = object.getString("name");
        boolean isBotPublic = object.getBoolean("bot_public");
        User owner = createUser(object.getObject("owner"));
        ApplicationTeam team = !object.isNull("team") ? createApplicationTeam(object.getObject("team")) : null;
        String customAuthUrl = object.getString("custom_install_url", null);
        List<String> tags = object.optArray("tags").orElseGet(DataArray::empty).stream(DataArray::getString)
                .collect(Collectors.toList());
        List<String> redirectUris = object.optArray("redirect_uris").orElseGet(DataArray::empty).stream(
                        DataArray::getString)
                .collect(Collectors.toList());
        String interactionsEndpointUrl = object.getString("interactions_endpoint_url", null);
        String roleConnectionsVerificationUrl = object.getString("role_connections_verification_url", null);

        Optional<DataObject> installParams = object.optObject("install_params");

        long defaultAuthUrlPerms =
                installParams.map(o -> o.getLong("permissions")).orElse(0L);

        List<String> defaultAuthUrlScopes = installParams
                .map(obj -> obj.getArray("scopes").stream(DataArray::getString).collect(Collectors.toList()))
                .orElse(Collections.emptyList());

        Optional<DataObject> integrationTypesConfigDict = object.optObject("integration_types_config");
        Map<IntegrationType, ApplicationInfo.IntegrationTypeConfiguration> integrationTypesConfig =
                integrationTypesConfigDict
                        .map(d -> {
                            Map<IntegrationType, ApplicationInfo.IntegrationTypeConfiguration> map =
                                    new EnumMap<>(IntegrationType.class);
                            for (String key : d.keys()) {
                                DataObject value = d.getObject(key);

                                ApplicationInfo.InstallParameters installParameters = value.optObject(
                                                "oauth2_install_params")
                                        .map(oauth2InstallParams -> new ApplicationInfoImpl.InstallParametersImpl(
                                                oauth2InstallParams.getArray("scopes").stream(DataArray::getString)
                                                        .collect(Collectors.toList()),
                                                Permission.getPermissions(oauth2InstallParams.getLong("permissions"))))
                                        .orElse(null);

                                map.put(
                                        IntegrationType.fromKey(key),
                                        new ApplicationInfoImpl.IntegrationTypeConfigurationImpl(installParameters));
                            }
                            return map;
                        })
                        .orElse(Collections.emptyMap());

        long approxUserInstallCount = object.getLong("approximate_user_install_count", -1);

        return new ApplicationInfoImpl(
                getJDA(),
                description,
                doesBotRequireCodeGrant,
                iconId,
                id,
                flags,
                isBotPublic,
                name,
                termsOfServiceUrl,
                privacyPolicyUrl,
                owner,
                team,
                tags,
                redirectUris,
                interactionsEndpointUrl,
                roleConnectionsVerificationUrl,
                customAuthUrl,
                defaultAuthUrlPerms,
                defaultAuthUrlScopes,
                approxUserInstallCount,
                integrationTypesConfig);
    }

    public ApplicationTeam createApplicationTeam(DataObject object) {
        String iconId = object.getString("icon", null);
        long id = object.getUnsignedLong("id");
        long ownerId = object.getUnsignedLong("owner_user_id", 0);
        List<TeamMember> members = map(object, "members", (o) -> {
            DataObject userJson = o.getObject("user");
            TeamMember.MembershipState state = TeamMember.MembershipState.fromKey(o.getInt("membership_state"));
            User user = createUser(userJson);
            TeamMember.RoleType roleType = user.getIdLong() == ownerId
                    ? TeamMember.RoleType.OWNER
                    : TeamMember.RoleType.fromKey(o.getString("role", ""));
            return new TeamMemberImpl(user, state, roleType, id);
        });
        return new ApplicationTeamImpl(iconId, members, id, ownerId);
    }

    public AuditLogEntry createAuditLogEntry(
            GuildImpl guild, DataObject entryJson, DataObject userJson, DataObject webhookJson) {
        long targetId = entryJson.getLong("target_id", 0);
        long userId = entryJson.getLong("user_id", 0);
        long id = entryJson.getLong("id");
        int typeKey = entryJson.getInt("action_type");
        DataArray changes = entryJson.isNull("changes") ? null : entryJson.getArray("changes");
        DataObject options = entryJson.isNull("options") ? null : entryJson.getObject("options");
        String reason = entryJson.getString("reason", null);

        UserImpl user = userJson == null ? null : createUser(userJson);
        WebhookImpl webhook = webhookJson == null ? null : createWebhook(webhookJson);
        Set<AuditLogChange> changesList;
        ActionType type = ActionType.from(typeKey);

        if (changes != null) {
            changesList = new HashSet<>(changes.length());
            for (int i = 0; i < changes.length(); i++) {
                DataObject object = changes.getObject(i);
                AuditLogChange change = createAuditLogChange(object);
                changesList.add(change);
            }
        } else {
            changesList = Collections.emptySet();
        }

        CaseInsensitiveMap<String, AuditLogChange> changeMap = new CaseInsensitiveMap<>(changeToMap(changesList));
        CaseInsensitiveMap<String, Object> optionMap =
                options != null ? new CaseInsensitiveMap<>(options.toMap()) : null;

        return new AuditLogEntry(
                type, typeKey, id, userId, targetId, guild, user, webhook, reason, changeMap, optionMap);
    }

    public AuditLogChange createAuditLogChange(DataObject change) {
        String key = change.getString("key");
        Object oldValue = change.isNull("old_value") ? null : change.get("old_value");
        Object newValue = change.isNull("new_value") ? null : change.get("new_value");
        return new AuditLogChange(oldValue, newValue, key);
    }

    public Entitlement createEntitlement(DataObject object) {
        return new EntitlementImpl(
                getJDA(),
                object.getUnsignedLong("id"),
                object.getUnsignedLong("sku_id"),
                object.getUnsignedLong("application_id"),
                object.getUnsignedLong("user_id", 0),
                object.getUnsignedLong("guild_id", 0),
                Entitlement.EntitlementType.fromKey(object.getInt("type")),
                object.getBoolean("deleted"),
                object.getOffsetDateTime("starts_at", null),
                object.getOffsetDateTime("ends_at", null),
                object.getBoolean("consumed", false));
    }

    private Map<String, AuditLogChange> changeToMap(Set<AuditLogChange> changesList) {
        return changesList.stream().collect(Collectors.toMap(AuditLogChange::getKey, UnaryOperator.identity()));
    }

    private <T> List<T> map(DataObject jsonObject, String key, Function<DataObject, T> convert) {
        if (jsonObject.isNull(key)) {
            return Collections.emptyList();
        }

        DataArray arr = jsonObject.getArray(key);
        List<T> mappedObjects = new ArrayList<>(arr.length());
        for (int i = 0; i < arr.length(); i++) {
            DataObject obj = arr.getObject(i);
            try {
                T result = convert.apply(obj);
                if (result != null) {
                    mappedObjects.add(result);
                }
            } catch (Exception e) {
                LOG.error("Failed to parse element in {} with content {}", key, obj, e);
            }
        }

        return mappedObjects;
    }
}
